Showing preview only (242K chars total). Download the full file or copy to clipboard to get everything.
Repository: yegor256/0pdd
Branch: master
Commit: 297730b34683
Files: 152
Total size: 211.3 KB
Directory structure:
gitextract_fz2x5scs/
├── .0pdd.yml
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── actionlint.yml
│ ├── bashate.yml
│ ├── codecov.yml
│ ├── copyrights.yml
│ ├── markdown-lint.yml
│ ├── pdd.yml
│ ├── plantuml.yml
│ ├── rake.yml
│ ├── reuse.yml
│ ├── shellcheck.yml
│ ├── typos.yml
│ ├── xcop.yml
│ └── yamllint.yml
├── .gitignore
├── .pdd
├── .rubocop.yml
├── .rultor.yml
├── 0pdd.rb
├── Aptfile
├── Gemfile
├── LICENSE.txt
├── LICENSES/
│ └── MIT.txt
├── Procfile
├── README.md
├── REUSE.toml
├── Rakefile
├── app.json
├── assets/
│ ├── sass/
│ │ └── main.sass
│ ├── upgrades/
│ │ ├── add-namespace.xsl
│ │ └── remove-broken-issues.xsl
│ ├── xsd/
│ │ └── puzzles.xsd
│ └── xsl/
│ ├── group.xsl
│ ├── join.xsl
│ ├── puzzles.xsl
│ ├── svg.xsl
│ ├── to-close.xsl
│ └── to-submit.xsl
├── config.ru
├── cucumber.yml
├── deploy.sh
├── dynamodb-local/
│ ├── config/
│ │ └── dynamo.yml
│ ├── pom.xml
│ └── tables/
│ └── 0pdd-events.json
├── features/
│ └── step_definitions/
│ └── steps.rb
├── model/
│ ├── README.md
│ ├── fake_weights_storage.rb
│ ├── linear.rb
│ ├── predictor.rb
│ ├── pso/
│ │ ├── lib/
│ │ │ ├── function.rb
│ │ │ ├── functions/
│ │ │ │ ├── rastrigin.rb
│ │ │ │ └── schwefel.rb
│ │ │ ├── solver.rb
│ │ │ ├── version.rb
│ │ │ └── zero_vector.rb
│ │ └── pso.rb
│ └── storage.rb
├── nginx.conf.sigil
├── objects/
│ ├── clients/
│ │ ├── github.rb
│ │ ├── gitlab.rb
│ │ └── jira.rb
│ ├── diff.rb
│ ├── dynamo.rb
│ ├── git_repo.rb
│ ├── invitations/
│ │ ├── github_invitations.rb
│ │ └── github_organization_invitations.rb
│ ├── jobs/
│ │ ├── job.rb
│ │ ├── job_commiterrors.rb
│ │ ├── job_detached.rb
│ │ ├── job_emailed.rb
│ │ ├── job_recorded.rb
│ │ └── job_starred.rb
│ ├── log.rb
│ ├── maybe_text.rb
│ ├── puzzles.rb
│ ├── storage/
│ │ ├── cached_storage.rb
│ │ ├── logged_storage.rb
│ │ ├── once_storage.rb
│ │ ├── s3.rb
│ │ ├── safe_storage.rb
│ │ ├── sync_storage.rb
│ │ ├── upgraded_storage.rb
│ │ └── versioned_storage.rb
│ ├── templates/
│ │ ├── github_tickets_body.haml
│ │ ├── gitlab_tickets_body.haml
│ │ └── jira_tickets_body.haml
│ ├── tickets/
│ │ ├── commit_tickets.rb
│ │ ├── emailed_tickets.rb
│ │ ├── logged_tickets.rb
│ │ ├── milestone_tickets.rb
│ │ ├── sentry_tickets.rb
│ │ ├── tagged_tickets.rb
│ │ └── tickets.rb
│ ├── truncated.rb
│ ├── user_error.rb
│ └── vcs/
│ ├── github.rb
│ ├── gitlab.rb
│ └── jira.rb
├── renovate.json
├── test/
│ ├── fake_github.rb
│ ├── fake_gitlab.rb
│ ├── fake_log.rb
│ ├── fake_repo.rb
│ ├── fake_storage.rb
│ ├── fake_tickets.rb
│ ├── test_0pdd.rb
│ ├── test__helper.rb
│ ├── test_cached_storage.rb
│ ├── test_commit_tickets.rb
│ ├── test_credentials.rb
│ ├── test_diff.rb
│ ├── test_diff_complicated.rb
│ ├── test_git_repo.rb
│ ├── test_github.rb
│ ├── test_github_invitations.rb
│ ├── test_github_tickets.rb
│ ├── test_gitlab.rb
│ ├── test_job.rb
│ ├── test_job_commiterrors.rb
│ ├── test_job_detached.rb
│ ├── test_job_emailed.rb
│ ├── test_log.rb
│ ├── test_logged_storage.rb
│ ├── test_logged_tickets.rb
│ ├── test_maybe_text.rb
│ ├── test_milestone_tickets.rb
│ ├── test_once_storage.rb
│ ├── test_puzzles.rb
│ ├── test_safe_storage.rb
│ ├── test_sentry_tickets.rb
│ ├── test_svg.rb
│ ├── test_truncated.rb
│ ├── test_upgraded_storage.rb
│ └── test_versioned_storage.rb
├── test-assets/
│ └── puzzles/
│ ├── closes-one-puzzle.xml
│ ├── ignores-unknown-issues.xml
│ ├── notify-unknown-open-issues.xml
│ ├── simple.xml
│ ├── submits-old-puzzles.xml
│ ├── submits-ranked-puzzles.xml
│ └── submits-three-tickets.xml
├── version.rb
└── views/
├── _footer.haml
├── _header.haml
├── error.haml
├── error_400.haml
├── index.haml
├── item.haml
├── layout.haml
├── log.haml
└── not_found.haml
================================================
FILE CONTENTS
================================================
================================================
FILE: .0pdd.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
errors:
- yegor256@gmail.com
# alerts:
# github:
# - yegor256
tags:
- pdd
- bug
================================================
FILE: .gitattributes
================================================
# Check out all text files in UNIX format, with LF as end of line
# Don't change this file. If you have any ideas about it, please
# submit a separate issue about it and we'll discuss.
* text=auto eol=lf
*.java ident
*.xml ident
*.png binary
================================================
FILE: .github/workflows/actionlint.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: actionlint
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
actionlint:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Download actionlint
id: get_actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
shell: bash
- name: Check workflow files
run: ${{ steps.get_actionlint.outputs.executable }} -color
shell: bash
================================================
FILE: .github/workflows/bashate.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: bashate
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
bashate:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.14
- run: pip install bashate
- run: |
readarray -t files < <(find . -name '*.sh')
bashate -i E006,E003 "${files[@]}"
================================================
FILE: .github/workflows/codecov.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: codecov
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
codecov:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- run: sudo apt-get install --yes libmagic-dev
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.9
bundler-cache: true
- run: bundle config set --global path "$(pwd)/vendor/bundle"
- run: bundle install --no-color
- run: bundle exec rake
- uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
================================================
FILE: .github/workflows/copyrights.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: copyrights
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
copyrights:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: yegor256/copyrights-action@0.0.12
================================================
FILE: .github/workflows/markdown-lint.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: markdown-lint
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
markdown-lint:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: DavidAnson/markdownlint-cli2-action@v23.2.0
================================================
FILE: .github/workflows/pdd.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: pdd
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
pdd:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: volodya-lombrozo/pdd-action@master
================================================
FILE: .github/workflows/plantuml.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: plantuml
'on':
push:
paths:
- '**.puml'
branches:
- master
permissions:
contents: write
jobs:
plantuml:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- name: Checkout Source
uses: actions/checkout@v6
- name: Generate SVG Diagrams
uses: holowinski/plantuml-github-action@main
with:
args: -v -tsvg doc/*.puml
- name: Commit changes
uses: EndBug/add-and-commit@v10
with:
author_name: ${{ github.actor }}
author_email: ${{ github.event.pusher.email }}
message: 'Diagram generated'
add: 'doc/*'
================================================
FILE: .github/workflows/rake.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: rake
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
strategy:
matrix:
os: [ubuntu-24.04]
ruby: [3.3]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- run: sudo apt-get install --yes libmagic-dev
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle config set --global path "$(pwd)/vendor/bundle"
- run: bundle install --no-color
- run: bundle exec rake
================================================
FILE: .github/workflows/reuse.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: reuse
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
reuse:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: fsfe/reuse-action@v6
================================================
FILE: .github/workflows/shellcheck.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: shellcheck
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
shellcheck:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: ludeeus/action-shellcheck@master
================================================
FILE: .github/workflows/typos.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: typos
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
typos:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: crate-ci/typos@v1.46.1
================================================
FILE: .github/workflows/xcop.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: xcop
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
xcop:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: g4s8/xcop-action@master
================================================
FILE: .github/workflows/yamllint.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
name: yamllint
'on':
push:
branches:
- master
pull_request:
branches:
- master
jobs:
yamllint:
timeout-minutes: 15
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: ibiqlik/action-yamllint@v3
================================================
FILE: .gitignore
================================================
*.gem
*.iml
.bundle/
.claude/
.DS_Store
.idea/
.sass-cache/
.yardoc/
coverage/
doc/
node_modules/
rdoc/
target/
vendor/
================================================
FILE: .pdd
================================================
--source=.
--verbose
--exclude README.md
--exclude coverage/**/*
--exclude assets/**/*
--exclude model/data/**/*
--rule min-words:10
--rule min-estimate:15
--rule max-estimate:90
================================================
FILE: .rubocop.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
AllCops:
Exclude:
- 'bin/**/*'
- 'assets/**/*'
- 'vendor/**/*'
DisplayCopNames: true
TargetRubyVersion: 2.6.0
NewCops: enable
SuggestExtensions: false
plugins:
- rubocop-rake
- rubocop-minitest
- rubocop-performance
Layout/MultilineOperationIndentation:
Enabled: false
Layout/EmptyLineAfterGuardClause:
Enabled: false
Naming/MethodParameterName:
MinNameLength: 1
Style/CommandLiteral:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Layout/IndentationWidth:
Enabled: false
Minitest/EmptyLineBeforeAssertionMethods:
Enabled: false
Layout/ElseAlignment:
Enabled: false
Naming/PredicateMethod:
Enabled: false
Layout/EndAlignment:
Enabled: false
Lint/RescueException:
Enabled: false
Metrics/MethodLength:
Max: 50
Metrics/ClassLength:
Max: 200
Exclude:
- "test/test_*.rb"
Metrics/AbcSize:
Max: 60
Metrics/BlockLength:
Max: 100
Layout/MultilineMethodCallIndentation:
Enabled: false
Metrics/CyclomaticComplexity:
Max: 11
Metrics/PerceivedComplexity:
Max: 11
Layout/LineLength:
Max: 120
Style/OpenStructUse:
Enabled: false
Style/ComparableClamp:
Enabled: false
================================================
FILE: .rultor.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
# yamllint disable rule:line-length
docker:
image: yegor256/rultor-image:1.24.0
assets:
config.yml: yegor256/home#assets/0pdd/config.yml
id_rsa: yegor256/home#assets/heroku-key
id_rsa.pub: yegor256/home#assets/heroku-key.pub
install: |-
git config --global user.email "server@0pdd.com"
git config --global user.name "0pdd.com"
sudo gem install pdd
pdd -f /dev/null
bundle install --no-color
release:
pre: false
sensitive:
- config.yml
script: |-
[[ "${tag}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit -1
bundle exec rake
git remote add dokku dokku@dokku.0pdd.com:zeropdd
rm -rf ~/.ssh
mkdir ~/.ssh
mv ../id_rsa ../id_rsa.pub ~/.ssh
chmod -R 600 ~/.ssh/*
echo -e "Host *\n StrictHostKeyChecking no\n UserKnownHostsFile=/dev/null" > ~/.ssh/config
git fetch
sed -i "s/BUILD/${tag}/g" ./version.rb
git add ./version.rb
git commit --no-verify -m 'build number set'
cp ../config.yml config.yml
git add config.yml
bundle exec ruby test/test_credentials.rb
git commit --no-verify -m 'config.yml'
git push -f dokku $(git symbolic-ref --short HEAD):master
git reset HEAD~1
rm -rf config.yml
curl -f --connect-timeout 15 -k --retry 5 --retry-delay 30 https://www.0pdd.com > /dev/null
merge:
script: |-
bundle exec rake
================================================
FILE: 0pdd.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
$stdout.sync = true
require 'glogin'
require 'haml'
require 'json'
require 'mail'
require 'net/http'
require 'octokit'
require 'ostruct'
require 'qbash'
require 'rack'
require 'sentry-ruby'
require 'sass'
require 'sinatra'
require 'sinatra/cookies'
require 'tmpdir'
require 'uri'
require_relative 'version'
require_relative 'objects/log'
require_relative 'objects/dynamo'
require_relative 'objects/git_repo'
require_relative 'objects/user_error'
require_relative 'objects/vcs/github'
require_relative 'objects/vcs/gitlab'
require_relative 'objects/clients/github'
require_relative 'objects/clients/gitlab'
require_relative 'objects/jobs/job'
require_relative 'objects/jobs/job_detached'
require_relative 'objects/jobs/job_emailed'
require_relative 'objects/jobs/job_recorded'
require_relative 'objects/jobs/job_starred'
require_relative 'objects/jobs/job_commiterrors'
require_relative 'objects/tickets/tickets'
require_relative 'objects/tickets/tagged_tickets'
require_relative 'objects/tickets/emailed_tickets'
require_relative 'objects/tickets/logged_tickets'
require_relative 'objects/tickets/commit_tickets'
require_relative 'objects/tickets/sentry_tickets'
require_relative 'objects/tickets/milestone_tickets'
require_relative 'objects/storage/s3'
require_relative 'objects/storage/safe_storage'
require_relative 'objects/storage/sync_storage'
require_relative 'objects/storage/logged_storage'
require_relative 'objects/storage/versioned_storage'
require_relative 'objects/storage/upgraded_storage'
require_relative 'objects/storage/cached_storage'
require_relative 'objects/storage/once_storage'
require_relative 'objects/invitations/github_invitations'
require_relative 'test/fake_storage'
configure do
Haml::Options.defaults[:format] = :xhtml
config = if ENV['RACK_ENV'] == 'test'
{
'testing' => true,
'github' => {
'token' => '--the-token--',
'client_id' => '?',
'client_secret' => '?'
},
'gitlab' => {
'token' => '--the-token--',
'client_id' => '?',
'client_secret' => '?'
},
'jira' => {
'token' => '--the-token--',
'client_id' => '?',
'client_secret' => '?'
},
'sentry' => '',
's3' => {
'region' => '?',
'bucket' => '?',
'key' => '?',
'secret' => '?'
},
'id_rsa' => ''
}
else
config = YAML.safe_load(File.open(File.join(File.dirname(__FILE__), 'config.yml')))
raise 'Missing configuration file config.yml' if config.nil?
config
end
if ENV['RACK_ENV'] != 'test'
Sentry.init do |c|
c.dsn = config['sentry']
c.release = VERSION
end
end
set :config, config
if config['smtp']
Mail.defaults do
delivery_method(
:smtp,
address: config['smtp']['host'],
port: config['smtp']['port'],
user_name: config['smtp']['user'],
password: config['smtp']['password'],
domain: '0pdd.com',
enable_starttls_auto: true
)
end
end
set :server_settings, timeout: 25
set :github, Github.new(config).client
set :gitlab, GitlabClient.new(config).client
set :dynamo, Dynamo.new(config).aws
set :glogin, GLogin::Auth.new(
config['github']['client_id'],
config['github']['client_secret'],
'https://www.0pdd.com/github-callback'
)
set :ruby_version, qbash('ruby -e "print RUBY_VERSION"')
set :git_version, qbash('git --version | cut -d" " -f 3')
set :temp_dir, Dir.mktmpdir('0pdd')
if ENV['RACK_ENV'] != 'test'
Thread.new do
loop do
sleep(10)
Net::HTTP.get_response(URI('https://www.0pdd.com/ping-github'))
rescue Exception
# If we reach this point, we must not even try to
# do anything. Here we must quietly ignore everything
# and let the daemon go to the next cycle.
end
end
end
end
use Rack::Deflater
# @todo #572:1h rewind is removed from rack 3.0, so it is moved to
# rewindableInput for now, but it is better to check another solutions
use Rack::RewindableInput::Middleware
before '/*' do
@locals = {
ver: VERSION,
login_link: settings.glogin.login_uri
}
if cookies[:glogin]
begin
@locals[:user] = GLogin::Cookie::Closed.new(
cookies[:glogin],
settings.config['github']['encryption_secret']
).to_user
rescue OpenSSL::Cipher::CipherError
@locals.delete(:user)
end
end
end
get '/github-callback' do
code = params[:code]
redirect('/') if code.nil?
cookies[:glogin] = GLogin::Cookie::Open.new(
settings.glogin.user(code),
settings.config['github']['encryption_secret']
).to_s
redirect to('/')
end
get '/logout' do
cookies.delete(:glogin)
redirect to('/')
end
get '/' do
projects = qbash(
"(sort /tmp/0pdd-done.txt 2>/dev/null || echo '')\
| uniq"
).split("\n").reject(&:empty?)
haml :index, layout: :layout, locals: merged(
title: '0pdd',
ruby_version: settings.ruby_version,
git_version: settings.git_version,
remaining: settings.github.rate_limit.remaining,
tail: projects.last(10).reverse
)
end
get '/robots.txt' do
'User-agent: *
Disallow: /snapshot'
end
get '/version' do
VERSION
end
get '/invitation' do
repo = repo_name(params[:repo])
ghi = GithubInvitations.new(settings.github)
invitations = ghi.accept_single_invitation(repo)
return invitations.join('\n') unless invitations.empty?
"Could not find invitation for @#{repo}. It is either invitation already
accepted OR 0pdd is not added as a collaborator"
end
get '/p' do
vcs = vcs_name(params[:vcs])
name = repo_name(params[:name])
xml = storage(name, vcs).load
Nokogiri::XSLT(File.read('assets/xsl/puzzles.xsl')).transform(
xml,
[
'version', "'#{VERSION}'",
'project', "'#{name}'",
'length', xml.to_s.length.to_s
]
).to_s
end
get '/xml' do
content_type 'text/xml'
vcs = vcs_name(params[:vcs])
storage(repo_name(params[:name]), vcs).load.to_s
end
get '/log' do
vcs = vcs_name(params[:vcs])
repo = repo_name(params[:name])
haml :log, layout: :layout, locals: merged(
title: repo,
repo: repo,
log: Log.new(settings.dynamo, repo, vcs),
since: params[:since] ? params[:since].to_i : Time.now.to_i + 1
)
end
get '/snapshot' do
content_type 'text/xml'
master = params[:branch]
vcs = vcs_name(params[:vcs])
name = repo_name(params[:name])
uri = "git@github.com:#{name}.git"
uri = "git@gitlab.com:#{name}.git" if vcs == 'gitlab'
begin
repo = GitRepo.new(
uri: uri,
name: name,
id_rsa: settings.config['id_rsa'],
dir: settings.temp_dir,
master: master || 'master'
)
repo.push
xml = repo.xml
xml.xpath('//processing-instruction("xml-stylesheet")').remove
xml.to_s
rescue StandardError => e
error 400, "Could not get snapshot for #{name}: #{e.message}"
end
end
get '/log-item' do
vcs = vcs_name(params[:vcs])
repo = repo_name(params[:repo])
tag = params[:tag]
error 404 if tag.nil?
log = Log.new(settings.dynamo, repo, vcs)
error 404 unless log.exists(tag)
haml :item, layout: :layout, locals: merged(
title: tag,
repo: repo,
item: log.get(tag)
)
end
get '/log-delete' do
redirect '/' if @locals[:user].nil? || @locals[:user][:login] != 'yegor256'
repo = repo_name(params[:name])
vcs = vcs_name(params[:vcs])
Log.new(settings.dynamo, repo, vcs).delete(params[:time].to_i, params[:tag])
redirect "/log?name=#{repo}"
end
get '/svg' do
response.headers['Cache-Control'] = 'no-cache, private'
content_type 'image/svg+xml'
name = repo_name(params[:name])
vcs = vcs_name(params[:vcs])
Nokogiri::XSLT(File.read('assets/xsl/svg.xsl')).transform(
storage(name, vcs).load, ['project', "'#{name}'"]
).to_s
end
get '/ping-github' do
content_type 'text/plain'
gh = settings.github
return if gh.rate_limit.remaining < 1000
invitations = GithubInvitations.new(gh)
invitations.accept
invitations.accept_orgs
msgs = gh.notifications.map do |n|
reason = n['reason']
repo = n['repository']['full_name']
puts "GitHub notification in #{repo}: #{reason} #{n['updated_at']} #{n['subject']['type']}"
if reason == 'mention'
issue = n['subject']['url'].gsub(%r{^.+/issues/}, '').to_i
comment = n['subject']['latest_comment_url'].gsub(%r{^.+/comments/}, '').to_i
begin
json = gh.issue_comment(repo, comment)
body = json['body']
if body.start_with?("@#{gh.login}") && json['user']['login'] != gh.login
gh.add_comment(
repo,
issue,
"> #{body.gsub(/\s+/, ' ').gsub(/^(.{100,}?).*$/m, '\1...')}\n\n" \
"I see you're talking to me, but I can't reply since I'm not a chat bot."
)
puts "Replied to #{repo}##{issue}"
end
rescue Octokit::NotFound => e
puts "Failed: #{e.message}"
next
end
end
"#{repo}: #{reason}"
end
gh.mark_notifications_as_read(last_read_at: Time.now)
"#{msgs.join("\n")}\n"
end
get '/hook/github' do
'This URL expects POST requests from GitHub
WebHook: https://developer.github.com/webhooks/'
end
post '/hook/github' do
is_from_github = request.env['HTTP_USER_AGENT']&.start_with?('GitHub-Hookshot')
is_push_event = request.env['HTTP_X_GITHUB_EVENT'] == 'push'
unless is_from_github && is_push_event
return [
400,
'Please, only register push events from GitHub webhook'
]
end
request.env['rack.input'].rewind if request.env['rack.input'].respond_to?(:rewind)
request.body.rewind unless request.env['rack.input'].respond_to?(:rewind)
json = JSON.parse(
case request.content_type
when 'application/x-www-form-urlencoded'
payload = params[:payload]
# see https://docs.github.com/en/webhooks/using-webhooks/creating-webhooks
if payload.nil?
return [
400,
'URL-encoded content is expected in the "payload" query parameter, but it is not provided'
]
end
payload
when 'application/json'
request.body.read
else
raise "Invalid content-type: \"#{request.content_type}\""
end
)
github = GithubRepo.new(settings.github, json, settings.config)
return [400, "No access to #{github.repo.name}"] unless github.exists?
unless ENV['RACK_ENV'] == 'test'
process_request(github) if github.repo.change_in_master?
puts "GitHub hook from #{github.repo.name} to branch #{github.repo.target}"
end
ignore = ''
ignore = 'Push is not to master branch, nothing is done. ' unless github.repo.change_in_master?
"#{ignore}Thanks #{github.repo.name}"
end
get '/hook/gitlab' do
'This URL expects POST requests from Gitlab
WebHook: https://docs.gitlab.com/ee/user/project/integrations/webhooks.html'
end
post '/hook/gitlab' do
is_from_gitlab = request.env['HTTP_USER_AGENT'].start_with?('GitLab')
is_push_event = request.env['HTTP_X_GITLAB_EVENT'] == 'Push Hook'
unless is_from_gitlab && is_push_event
return [
400,
'Please, only register push events from Gitlab webhook'
]
end
request.env['rack.input'].rewind if request.env['rack.input'].respond_to?(:rewind)
request.body.rewind unless request.env['rack.input'].respond_to?(:rewind)
json = JSON.parse(
case request.content_type
when 'application/x-www-form-urlencoded'
params[:payload]
when 'application/json'
request.body.read
else
raise "Invalid content-type: \"#{request.content_type}\""
end
)
gitlab = GitlabRepo.new(settings.gitlab, json, settings.config)
return [400, "No access to #{gitlab.repo.name}"] unless gitlab.exists?
unless ENV['RACK_ENV'] == 'test'
process_request(gitlab) if gitlab.repo.change_in_master?
puts "Gitlab hook from #{gitlab.repo.name} to branch #{gitlab.repo.target}"
end
ignore = ''
ignore = 'Push is not to master branch, nothing is done. ' unless gitlab.repo.change_in_master?
"#{ignore}Thanks #{gitlab.repo.name}"
end
get '/css/*.css' do
content_type 'text/css', charset: 'utf-8'
file = params[:splat].first
template = File.join(File.absolute_path('./assets/sass/'), "#{file}.sass")
Sass::Engine.new(File.read(template)).render
end
get '/puzzles.xsd' do
content_type 'application/xml', charset: 'utf-8'
File.read('assets/xsd/puzzles.xsd')
end
not_found do
status 404
content_type 'text/html', charset: 'utf-8'
haml :not_found, layout: :layout, locals: merged(
title: 'Page not found'
)
end
error do
status 503
e = env['sinatra.error']
Sentry.capture_exception(e) unless e.is_a?(UserError)
haml(
:error,
layout: :layout,
locals: merged(
title: 'error',
error: "#{e.message}\n\t#{e.backtrace.join("\n\t")}"
)
)
end
def repo_name(name)
error 404 if name.nil?
error 404 unless %r{^[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_.]+$}.match?(name)
name.strip
end
def vcs_name(name)
return 'github' if name.nil?
name.strip.downcase
end
def merged(hash)
out = @locals.merge(hash)
out[:local_assigns] = out
out
end
def storage(repo, vcs)
file_name = vcs == 'github' ? repo : "#{vcs}-#{repo}"
SyncStorage.new(
UpgradedStorage.new(
SafeStorage.new(
OnceStorage.new(
CachedStorage.new(
VersionedStorage.new(
if ENV['RACK_ENV'] == 'test'
FakeStorage.new
else
LoggedStorage.new(
S3.new(
"#{file_name}.xml",
settings.config['s3']['bucket'],
settings.config['s3']['region'],
settings.config['s3']['key'],
settings.config['s3']['secret']
),
Log.new(settings.dynamo, repo, vcs)
)
end,
VERSION
),
File.join('/tmp/0pdd-xml-cache', file_name)
)
)
),
VERSION
)
)
end
def process_request(vcs)
JobDetached.new(
vcs,
JobCommitErrors.new(
vcs,
JobEmailed.new(
vcs,
JobRecorded.new(
vcs,
JobStarred.new(
vcs,
Job.new(
vcs,
storage(vcs.repo.name, vcs.name),
SentryTickets.new(
EmailedTickets.new(
vcs,
CommitTickets.new(
vcs,
TaggedTickets.new(
vcs,
LoggedTickets.new(
vcs,
Log.new(settings.dynamo, vcs.repo.name, vcs.name),
MilestoneTickets.new(
vcs,
Tickets.new(vcs)
)
)
)
)
)
)
)
)
)
)
)
).proceed
end
================================================
FILE: Aptfile
================================================
git
================================================
FILE: Gemfile
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
source 'https://rubygems.org'
gem 'atlassian-jwt', '~>0.2.1'
gem 'aws-sdk-dynamodb', '~>1.111'
gem 'aws-sdk-s3', '~>1.176'
gem 'crack', '~>1.0'
gem 'faraday', '~>2.14'
gem 'gitlab', '~>6.0'
gem 'glogin', '~>0.16'
gem 'haml', '~>5.2'
gem 'jira-ruby', '~>3.0'
gem 'mail', '~>2.8'
gem 'matrix', '~>0.4'
gem 'minitest', '~>6.0', require: false
gem 'minitest-reporters', '~>1.7', require: false
gem 'net-smtp', '~>0.5'
gem 'nokogiri', '~>1.18'
gem 'octokit', '~>10.0'
gem 'ostruct', '~>0.6'
gem 'pdd', '~>0.24'
gem 'qbash', '~>0.4'
gem 'rack', '~>3.1'
gem 'rack-test', '~>2.2'
gem 'rackup', '~>2.2'
gem 'rake', '~>13.2', require: false
gem 'rubocop', '~>1.69', require: false
gem 'rubocop-minitest', '~>0.38', require: false
gem 'rubocop-performance', '~>1.26', require: false
gem 'rubocop-rake', '~>0.7', require: false
gem 'sass', '~>3.7'
gem 'sentry-ruby', '~>6.2'
gem 'simplecov', '~>0.22'
gem 'simplecov-cobertura', '~>3.1'
gem 'sinatra', '~>4.0'
gem 'sinatra-contrib', '~>4.0'
gem 'sprockets', '~>4.2'
gem 'veils', '~>0.4'
gem 'webrick', '~>1.9'
gem 'xcop', '~>0.7'
================================================
FILE: LICENSE.txt
================================================
(The MIT License)
Copyright (c) 2016-2026 Yegor Bugayenko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the 'Software'), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: LICENSES/MIT.txt
================================================
(The MIT License)
Copyright (c) 2016-2026 Yegor Bugayenko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the 'Software'), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Procfile
================================================
web: bundle exec rackup config.ru -p $PORT
cron: curl -s https://www.0pdd.com/ping-github
================================================
FILE: README.md
================================================
# Puzzle Driven Development (PDD) GitHub Chatbot
[](https://www.elegantobjects.org)
[](https://www.rultor.com/p/yegor256/0pdd)
[](https://www.jetbrains.com/ruby/)
[](https://github.com/yegor256/0pdd/actions/workflows/rake.yml)
[](https://www.sixnines.io/h/574a)
[](https://www.rehttp.net/i?u=http%3A%2F%2Fwww.0pdd.com%2Fhook%2Fgithub)
[](https://www.0pdd.com/p?name=yegor256/0pdd)
[](https://codeclimate.com/github/yegor256/0pdd/maintainability)
[](https://codecov.io/github/yegor256/0pdd?branch=master)
[](https://hitsofcode.com/view/github/yegor256/0pdd)
[](https://www.codacy.com/gh/yegor256/0pdd/dashboard)
Read this blog post first:
[PDD in Action](https://www.yegor256.com/2017/04/05/pdd-in-action.html).
TL;DR:
1. Your boss tells you to fix issue `#42`
1. You do it, but not completely (you have no time, you are lazy, etc)
1. You put `TODO #42:30min bla-bla-bla` into the codebase (in a pull request)
1. CI checks that you didn't break the format of the `TODO`
(reuse our [`pdd.yml`][pdd.yml])
1. You merge the pull request
1. The bot picks up the `TODO` and creates issue `#43` (new one)
1. The boss asks your friend to fix `#43`
1. The friend fixes it, and merges
1. The `TODO` is gone from the codebase
1. The bot closes the issue `#43`
[0pdd.com](https://www.0pdd.com) is a hosted service that
finds new "puzzles" in your repository and posts them as GitHub
issues. To start using it just create a
[Webhook](https://developer.github.com/webhooks/creating/) in your repository
just for `push` events with `https://www.0pdd.com/hook/github` payload URL and
`application/json` content type.
Then, add [@0pdd](https://github.com/0pdd) GitHub user as a
[collaborator] to your repository, if it's private
(you don't need this for a public repository).
If your invitation is not accepted by [@0pdd](https://github.com/0pdd)
within 30 minutes, visit this address:
`https://0pdd.com/invitation?repo={REPO_FULL_NAME}`, where `REPO_FULL_NAME`
is the full name of your repo, e.g., `yegor256/0pdd`.
Then, add a `@todo` [puzzle](https://www.yegor256.com/2009/03/04/pdd.html)
to the source code (format it [right](https://github.com/teamed/pdd)).
Then, `git push` something to the master branch and see what happens.
You should see a new
issue created in your repository by [@0pdd](https://github.com/0pdd).
You can find the dependency tree of all puzzles in your repository
here: `https://www.0pdd.com/p?name=yegor256/0pdd` (just replace the name
of the repo in the URL).
Don't forget to add that cute little badge to your `README.md`, just
like we did here in this repo (see above). The Markdown you need
will look like this (replace `yegor256/0pdd` with GitHub coordinates
of your own repository):
```markdown
[](https://www.0pdd.com/p?name=yegor256/0pdd)
```
## How to configure?
The only way to configure 0pdd is to add `.0pdd.yml` file to the
root directory of your `master` branch (see
[this one](https://github.com/yegor256/0pdd/blob/master/.0pdd.yml)
as a live example).
It has to be a [YAML](https://en.wikipedia.org/wiki/YAML)
file with the following
optional parameters inside:
```yaml
threshold: 10
model: true
errors:
- yegor256@gmail.com
alerts:
suppress:
- on-found-puzzle
- on-lost-puzzle
- on-scope
github:
- yegor256
format:
- short-title
- title-length=100
tags:
- pdd
- bug
```
The element `threshold` allows you to limit the number of issues created
from the puzzles in your code. In the example above, each time the appropriate
push event is sent to your webhook up to 10 issues will be created regardless
of the number of puzzles found in the code. If this limit is not set,
`threshold` is assumed to be equal to 256.
Section `errors` allows you to specify a list of email addresses that will
receive notifications when PDD processing fails for your repo. This is
a useful feature, since programmers often make
mistakes in PDD puzzle formatting. We recommend using it.
Section `alerts` allows you to specify users that will be notified when
new PDD puzzles show up. By default we will just submit GitHub tickets
and that's it. If you add `github` subsection there, you can list GitHub
users who will be "notified": their GitHub nicknames will be added to
each puzzle description and GitHub will notify them by email.
Subsection `suppress` lets you make 0pdd quieter where necessary:
* `on-found-puzzle`: stay quiet when a new puzzle is discovered
* `on-lost-puzzle`: stay quiet when a puzzle is gone
* `on-scope`: stay quiet when child puzzles change statuses
The `model` option is used by 0pdd
to opt in to an ML model that prioritizes puzzles generated by `pdd`.
If you would like to opt in to puzzle prioritization, add this option
to your `.0pdd.yml`.
[pdd](https://github.com/yegor256/pdd) is the tool that parses your source
code files. You can configure its behavior by adding `.pdd` file to the
root directory of the repository. Take
[this one](https://github.com/yegor256/0pdd/blob/master/.pdd), as an example.
The `format` section helps you instruct 0pdd about GitHub issues formatting.
These options are supported:
* `short-title`: issue title will not include file name and line numbers
* `title-length=...`: you may configure the length of the title of GitHub
issues we create. Minimum length is 30, maximum is 255. Any other values
will be silently ignored. The default length is 60.
The `tags` section lists GitHub labels that will automatically be attached
to all new issues we create. If you don't have those labels in your GitHub
repository, they will automatically be created.
To exclude files from analysis, create a `.pdd` file with the following content:
```text
--exclude=path/to/file.txt
```
See: [pdd usage](https://github.com/cqfn/pdd?tab=readme-ov-file#usage)
## What to expect?
Pay attention to the comments @0pdd posts to your commits. They will
contain valuable information about its recent actions. If something goes
wrong, you will receive exception messages there. Please, post them here
as new issues.
Remember that GitHub triggers us only when you do `git push`. This means that
if you make a number of commits, we will process them all together. Only the
latest one will be commented. It may not be the one with new puzzles though.
After we create GitHub issues you can modify their titles and descriptions. You
can work with them as with any other issues. We will touch them only one
more time, when the puzzle disappears from the source code. At that moment
we will try to close the issue. If it is already closed, nothing will happen.
However, it's not a good practice to close them manually. You better remove
the necessary puzzle from the source code and let us close the issue.
## How to contribute?
It is a Ruby project.
First, install
[Java] SDK 8+,
[Maven 3.2+](https://maven.apache.org/),
[Ruby 2.3+](https://www.ruby-lang.org/en/documentation/installation/),
[Rubygems](https://rubygems.org/pages/download),
and
[Bundler](https://bundler.io/).
Then:
```bash
bundle update
bundle exec rake
```
The build has to be clean. If it's not,
[submit an issue](https://github.com/yegor256/0pdd/issues).
Then, make your changes, make sure the build is still clean,
and [submit a pull request][guidelines].
To run it locally:
```bash
bundle exec rake run
```
If you want to run it on your own machine, you will need to add this
`config.yml` file to the root directory of this repository:
```yaml
s3:
region: us-east-1
bucket: xml.0pdd.com
key: AKIAI..........UTSQA
secret: Z2FbKB..........viCKaYo4H..........vva21
sentry: https://....@sentry.io/229223
dynamo:
region: us-east-1
key: AKIAI..........UTSQA
secret: Z2FbKB..........viCKaYo4H..........vva21
github:
client_id: b96a3b5..........87e
client_secret: be61c471154e2..........66f434d33e0f63a5f
encryption_secret: some-random-text
login: 0pdd
token: GitHub-Password
smtp:
host: email-smtp.us-east-1.amazonaws.com
port: 587
user: smtp_user
password: smtp_password
id_rsa: |
... RSA key goes here, in ASCII format
```
We add this file to the repository while deploying to Heroku,
see how it's done in `.rultor.yml`.
## How to install in Heroku
Don't forget this:
```bash
heroku buildpacks:add --index 1 https://github.com/heroku/heroku-buildpack-apt
```
[pdd.yml]: https://github.com/yegor256/0pdd/blob/master/.github/workflows/pdd.yml
[guidelines]: https://www.yegor256.com/2014/04/15/github-guidelines.html
[Java]: https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
[collaborator]: https://help.github.com/articles/inviting-collaborators-to-a-personal-repository/
================================================
FILE: REUSE.toml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
# SPDX-License-Identifier: MIT
version = 1
[[annotations]]
path = [
".DS_Store",
".gitattributes",
".gitignore",
".pdd",
"**.json",
"**.md",
"**.png",
"**.sigil",
"**.svg",
"**.txt",
"**/.DS_Store",
"**/.gitignore",
"**/.pdd",
"**/*.csv",
"**/*.jpg",
"**/*.json",
"**/*.md",
"**/*.pdf",
"**/*.png",
"**/*.svg",
"**/*.txt",
"**/*.vm",
"**/CNAME",
"**/Gemfile.lock",
"**/Procfile",
"Aptfile",
"doc/integration.puml",
"Gemfile.lock",
"nginx.conf.sigil",
"Procfile",
"README.md",
"renovate.json",
]
precedence = "override"
SPDX-FileCopyrightText = "Copyright (c) 2025 Yegor Bugayenko"
SPDX-License-Identifier = "MIT"
================================================
FILE: Rakefile
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'rubygems'
require 'rake'
require 'rake/clean'
require_relative 'objects/dynamo'
ENV['RACK_ENV'] = 'test'
task default: %i[clean test rubocop xcop]
require 'rake/testtask'
desc 'Run all unit tests'
Rake::TestTask.new(test: :dynamo) do |test|
Rake::Cleaner.cleanup_files(['coverage'])
test.libs << 'lib' << 'test'
test.pattern = 'test/**/test_*.rb'
test.verbose = false
test.warning = false
end
require 'rubocop/rake_task'
desc 'Run RuboCop on all directories'
RuboCop::RakeTask.new(:rubocop) do |task|
task.fail_on_error = true
end
require 'xcop/rake_task'
desc 'Validate all XML/XSL/XSD/HTML files for formatting'
Xcop::RakeTask.new :xcop do |task|
task.includes = ['**/*.xml', '**/*.xsl', '**/*.xsd', '**/*.html']
task.excludes = ['target/**/*', 'coverage/**/*', 'vendor/**/*']
end
desc 'Start DynamoDB Local server'
task :dynamo do
FileUtils.rm_rf('dynamodb-local/target')
pid = Process.spawn('mvn', 'install', '--quiet', chdir: 'dynamodb-local')
at_exit do
`kill -TERM #{pid}`
puts "DynamoDB Local killed in PID #{pid}"
end
begin
status = Dynamo.new.aws.describe_table(
table_name: '0pdd-events'
)[:table][:table_status]
puts "DynamoDB Local table: #{status}"
rescue Exception => e
puts e.message
sleep(5)
retry
end
puts "DynamoDB Local is running in PID #{pid}"
end
desc 'Sleep endlessly after the start of DynamoDB Local server'
task :sleep do
loop do
sleep(5)
puts 'Still alive...'
end
end
desc 'Run website'
task run: :dynamo do
`rerun -b "RACK_ENV=test rackup"`
end
================================================
FILE: app.json
================================================
{
"healthchecks": {
"web": [
{
"attempts": 3,
"description": "Checking if the app responds to the /robots.txt endpoint",
"name": "web check",
"path": "/robots.txt",
"type": "startup"
}
]
}
}
================================================
FILE: assets/sass/main.sass
================================================
// SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
// SPDX-License-Identifier: MIT
body
background-color: white
color: #111
font-family: monospace
font-size: 18px
margin: 0
padding: 1em
a
color: blue
&:hover
color: inherit
.logo
height: 92px
width: 92px
.center
height: 19em
left: 0
margin: auto
max-width: 100%
position: absolute
right: 0
text-align: center
width: 20em
.versions
img
height: 1em
================================================
FILE: assets/upgrades/add-namespace.xsl
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" exclude-result-prefixes="xs">
<xsl:output method="xml"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/puzzles">
<puzzles xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://www.0pdd.com/puzzles.xsd">
<xsl:apply-templates select="node()|@*"/>
</puzzles>
</xsl:template>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
================================================
FILE: assets/upgrades/remove-broken-issues.xsl
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" exclude-result-prefixes="xs">
<xsl:output method="xml"/>
<xsl:strip-space elements="*"/>
<xsl:template match="puzzle/issue[string-length(.) = 0]">
<!-- This issue is broken, we just don't copy it -->
</xsl:template>
<xsl:template match="puzzle/issue/@href[string-length(.) = 0]">
<!-- This HREF is broken, we just don't copy it -->
</xsl:template>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
================================================
FILE: assets/xsd/puzzles.xsd
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:simpleType name="issue_name">
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]+|[A-Z]+-[0-9]+|unknown"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="puzzle">
<xs:all>
<xs:element name="id" minOccurs="1" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[a-zA-Z0-9\-]+-[a-f0-9]{8}"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="issue" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="issue_name">
<xs:attribute name="model" type="xs:integer" use="optional"/>
<xs:attribute name="href" type="xs:anyURI" use="optional"/>
<xs:attribute name="closed" use="optional" type="xs:dateTime"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="body" minOccurs="1" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="lines" minOccurs="1" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]+-[0-9]+"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="file" minOccurs="1" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value=".+"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="estimate" minOccurs="1" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:integer">
<xs:minInclusive value="0"/>
<xs:maxInclusive value="60000"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="ticket" minOccurs="1" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[a-zA-Z0-9\-]+"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="role" minOccurs="1" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[A-Z]+"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="author" minOccurs="1" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value=".+"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="email" minOccurs="1" maxOccurs="1">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="([0-9a-zA-Z]([-_.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="time" minOccurs="1" maxOccurs="1" type="xs:dateTime"/>
<xs:element name="children" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence>
<xs:element name="puzzle" type="puzzle" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="alive" use="required" type="xs:boolean"/>
</xs:complexType>
<xs:element name="puzzles">
<xs:complexType>
<xs:sequence>
<xs:element name="puzzle" type="puzzle" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="date" use="required" type="xs:dateTime"/>
<xs:attribute name="model" use="optional" type="xs:boolean"/>
<xs:attribute name="version" use="required">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="[0-9\.]+|BUILD"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
<xs:unique name="puzzleId">
<xs:selector xpath=".//puzzle"/>
<xs:field xpath="@id"/>
</xs:unique>
</xs:element>
</xs:schema>
================================================
FILE: assets/xsl/group.xsl
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" exclude-result-prefixes="xs">
<xsl:output method="xml"/>
<xsl:strip-space elements="*"/>
<xsl:key name="issues" match="//puzzle" use="issue"/>
<xsl:key name="roots" match="//puzzle[not(key('issues',ticket))]" use="id"/>
<xsl:template match="/puzzles">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates select="//puzzle[key('roots',id)]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="puzzle">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<children>
<xsl:apply-templates select="//puzzle[ticket=current()/issue]"/>
</children>
</xsl:copy>
</xsl:template>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
================================================
FILE: assets/xsl/join.xsl
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" exclude-result-prefixes="xs">
<xsl:output method="xml"/>
<xsl:strip-space elements="*"/>
<xsl:key name="existing" match="//puzzle" use="id"/>
<xsl:key name="extras" match="//extra" use="id"/>
<xsl:template match="/puzzles">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates select="//puzzle[id!='unknown']"/>
<xsl:apply-templates select="//extra[not(key('existing',id))]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="puzzle|extra">
<puzzle>
<xsl:attribute name="alive">
<xsl:choose>
<xsl:when test="key('extras',id)">
<xsl:text>true</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>false</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:choose>
<xsl:when test="issue">
<xsl:apply-templates select="issue"/>
</xsl:when>
<xsl:otherwise>
<issue>
<xsl:text>unknown</xsl:text>
</issue>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates select="ticket|estimate|role|id|lines|body|file|author|email|time"/>
</puzzle>
</xsl:template>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
================================================
FILE: assets/xsl/puzzles.xsl
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml" version="1.0">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:param name="version"/>
<xsl:param name="project"/>
<xsl:param name="length"/>
<xsl:template match="/puzzles">
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="{$project}"/>
<meta name="keywords" content="{$project}"/>
<meta name="author" content="0pdd.com"/>
<title>
<xsl:value-of select="$project"/>
</title>
<link type="text/css" href="/css/main.css" rel="stylesheet"/>
<link rel="shortcut icon" href="https://avatars2.githubusercontent.com/u/24456188"/>
</head>
<body>
<p>
<a href="https://www.0pdd.com">
<img class="logo" src="https://avatars2.githubusercontent.com/u/24456188"/>
</a>
</p>
<p>
<img src="/svg?name={$project}"/>
</p>
<p>
<xsl:value-of select="count(//puzzle[@alive='true'])"/>
<xsl:text> alive, </xsl:text>
<xsl:value-of select="count(//puzzle)"/>
<xsl:text> total.</xsl:text>
</p>
<xsl:apply-templates select="puzzle"/>
<p>
<xsl:text>--</xsl:text>
</p>
<p>
<xsl:text>Full </xsl:text>
<a href="/log?name={$project}">
<xsl:text>log</xsl:text>
</a>
<xsl:text> of recent events.</xsl:text>
</p>
<p>
<xsl:text>Download </xsl:text>
<a href="/xml?name={$project}">
<xsl:text>XML</xsl:text>
</a>
<xsl:text> (</xsl:text>
<span title="{$length} bytes">
<xsl:value-of select="format-number($length div 1024, '#.0')"/>
<xsl:text> Kb</xsl:text>
</span>
<xsl:text>); see </xsl:text>
<a href="/snapshot?name={$project}">
<xsl:text>snapshot</xsl:text>
</a>
<xsl:text>.</xsl:text>
</p>
<p>
<xsl:text>Project "</xsl:text>
<xsl:value-of select="$project"/>
<xsl:text>" updated by </xsl:text>
<a href="https://www.0pdd.com">
<xsl:text>0pdd</xsl:text>
</a>
<xsl:text> v</xsl:text>
<xsl:value-of select="@version"/>
<xsl:text> on </xsl:text>
<xsl:value-of select="@date"/>
<xsl:text>.</xsl:text>
</p>
<p>
<a href="https://www.0pdd.com" title="Current version of 0pdd is {$version}">
<xsl:value-of select="$version"/>
</a>
</p>
</body>
</html>
</xsl:template>
<xsl:template match="puzzle">
<div>
<span>
<xsl:if test="@alive = 'false'">
<xsl:attribute name="style">
<xsl:text>color:gray;</xsl:text>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="id" mode="fonted"/>
<xsl:text> </xsl:text>
<xsl:value-of select="file"/>
<xsl:text>:</xsl:text>
<xsl:value-of select="lines"/>
<xsl:text> </xsl:text>
<xsl:value-of select="estimate"/>
<xsl:text>min </xsl:text>
</span>
<xsl:if test="children/puzzle">
<div style="margin-left: 2em;">
<xsl:apply-templates select="children/puzzle"/>
</div>
</xsl:if>
</div>
</xsl:template>
<xsl:template match="id" mode="fonted">
<xsl:choose>
<xsl:when test="../@alive='true'">
<xsl:apply-templates select="." mode="linked"/>
</xsl:when>
<xsl:otherwise>
<strike>
<xsl:apply-templates select="." mode="linked"/>
</strike>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="id" mode="linked">
<xsl:choose>
<xsl:when test="../issue/@href">
<a href="{../issue/@href}" style="color:inherit">
<xsl:value-of select="."/>
</a>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
================================================
FILE: assets/xsl/svg.xsl
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/2000/svg" version="1.0">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:template match="/puzzles">
<xsl:variable name="alive" select="count(//puzzle[@alive='true'])"/>
<xsl:variable name="total" select="count(//puzzle)"/>
<xsl:variable name="count" select="concat($alive, '/', $total)"/>
<xsl:variable name="advance" select="47 + (string-length($count) * 6.5) + 7"/>
<xsl:variable name="width">
<xsl:choose>
<xsl:when test="$advance > 86">
<xsl:value-of select="ceiling($advance)"/>
</xsl:when>
<xsl:otherwise>
<xsl:text>86</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<svg width="{$width}" height="20">
<linearGradient id="b" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<mask id="a">
<rect width="{$width}" height="20" rx="3" fill="#fff"/>
</mask>
<g mask="url(#a)">
<path fill="#555" d="M0 0h47v20H0z"/>
<path fill="#4c1" d="M47 0h{$width - 47}v20H47z"/>
<path fill="url(#b)" d="M0 0h{$width}v20H0z"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="19.5" y="15" fill="#010101" fill-opacity=".3">0pdd</text>
<text x="19.5" y="14">0pdd</text>
<text x="{$width - 3.5}" y="15" fill="#010101" fill-opacity=".3" text-anchor="end">
<xsl:value-of select="$count"/>
</text>
<text x="{$width - 3.5}" y="14" text-anchor="end">
<xsl:value-of select="$count"/>
</text>
</g>
</svg>
</xsl:template>
</xsl:stylesheet>
================================================
FILE: assets/xsl/to-close.xsl
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" exclude-result-prefixes="xs">
<xsl:output method="xml"/>
<xsl:strip-space elements="*"/>
<xsl:key name="extras" match="//extra" use="id"/>
<xsl:template match="/puzzles">
<xsl:copy>
<xsl:apply-templates select="//puzzle[@alive='true' and not(key('extras',id)) and issue!='unknown']"/>
</xsl:copy>
</xsl:template>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
================================================
FILE: assets/xsl/to-submit.xsl
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" exclude-result-prefixes="xs">
<xsl:output method="xml"/>
<xsl:strip-space elements="*"/>
<xsl:key name="existing" match="//puzzle[@alive='true']" use="id"/>
<xsl:template match="/puzzles">
<xsl:copy>
<xsl:apply-templates select="//extra[not(key('existing',id))]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="extra">
<puzzle>
<xsl:apply-templates select="@*|node()"/>
</puzzle>
</xsl:template>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
================================================
FILE: config.ru
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require './0pdd'
$stdout.sync = true
run Sinatra::Application
================================================
FILE: cucumber.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
default: --format pretty
travis: --format progress
html_report: --format progress --format html --out=features_report.html
================================================
FILE: deploy.sh
================================================
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
set -e -o pipefail
cd "$(dirname "$0")"
bundle update
sed -i -s 's|Gemfile.lock||g' .gitignore
cp /code/home/assets/0pdd/config.yml .
git add config.yml
git add Gemfile.lock
git add .gitignore
git commit --no-verify -m 'config.yml for heroku'
trap 'git reset HEAD~1 && rm config.yml && git checkout -- .gitignore' EXIT
git push heroku master -f
================================================
FILE: dynamodb-local/config/dynamo.yml
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
---
port: ${dynamo.port}
key: ${dynamo.key}
secret: ${dynamo.secret}
================================================
FILE: dynamodb-local/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.0pdd</groupId>
<artifactId>dynamodb-local</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>dynamodb-local</name>
<properties>
<dynamo.key>AAAAABBBBBAAAAABBBBB</dynamo.key>
<dynamo.secret>ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD</dynamo.secret>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack-dynamodb-local</id>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>com.jcabi</groupId>
<artifactId>DynamoDBLocal</artifactId>
<version>2023-05-26</version>
<type>zip</type>
<outputDirectory>${project.build.directory}/dynamodb-dist</outputDirectory>
<overWrite>false</overWrite>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>reserver-dynamodb-port</id>
<goals>
<goal>reserve-network-port</goal>
</goals>
<configuration>
<portNames>
<portName>dynamo.port</portName>
</portNames>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>pre-integration-test</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>${basedir}/config</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-dynamodb-maven-plugin</artifactId>
<version>0.10.1</version>
<executions>
<execution>
<id>dynamodb-integration-test</id>
<goals>
<goal>start</goal>
<goal>create-tables</goal>
<goal>wait</goal>
</goals>
<configuration>
<port>${dynamo.port}</port>
<dist>${project.build.directory}/dynamodb-dist</dist>
<key>${dynamo.key}</key>
<secret>${dynamo.secret}</secret>
<arguments>
<argument>-inMemory</argument>
</arguments>
<tables>
<table>${basedir}/tables/0pdd-events.json</table>
</tables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
================================================
FILE: dynamodb-local/tables/0pdd-events.json
================================================
{
"AttributeDefinitions": [
{
"AttributeName": "repo",
"AttributeType": "S"
},
{
"AttributeName": "time",
"AttributeType": "N"
},
{
"AttributeName": "tag",
"AttributeType": "S"
}
],
"GlobalSecondaryIndexes": [
{
"IndexName": "tags",
"KeySchema": [
{
"AttributeName": "repo",
"KeyType": "HASH"
},
{
"AttributeName": "tag",
"KeyType": "RANGE"
}
],
"Projection": {
"ProjectionType": "ALL"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": "1",
"WriteCapacityUnits": "1"
}
}
],
"KeySchema": [
{
"AttributeName": "repo",
"KeyType": "HASH"
},
{
"AttributeName": "time",
"KeyType": "RANGE"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": "1",
"WriteCapacityUnits": "1"
},
"TableName": "0pdd-events"
}
================================================
FILE: features/step_definitions/steps.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'tmpdir'
require 'English'
Before do
@cwd = Dir.pwd
@dir = Dir.mktmpdir('test')
FileUtils.mkdir_p(@dir)
Dir.chdir(@dir)
end
After do
Dir.chdir(@cwd)
FileUtils.rm_rf(@dir)
end
================================================
FILE: model/README.md
================================================
Puzzle Ranking (Linear ML Model)
### Internals
The ML model is a linear model with PSO optimizer.
The optimizer is used to train the model on puzzle data,
the weights are stored and used to predict future puzzles.
Because of the time required, training is a non-blocking process,
and puzzle prioritization uses a naive ranking approach based on puzzle estimate.
Subsequent events use the linear model for prioritization.
The linear model is the external API for the model.
It has one method `predict(...)` which accepts an array of puzzles in xml.
The output of this model is an array of positional index of the input puzzles:
```ruby
# usage
rank = LinearModel.new(repo_name, storage).predict(puzzles)
# repo_name -> name of repository
# storage -> storage object (with defined interface)
# puzzles -> array of xml puzzles.
#
# rank -> array of positional index of ranked puzzles
```
### Integration
This diagram shows how this model can be integrated into 0pdd workflow:

================================================
FILE: model/fake_weights_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# FakeWeightsStorage
#
class FakeWeightsStorage
def initialize(
repo,
dir = Dir.mktmpdir
)
@file = File.join(dir, "#{repo}.marshal")
end
def load
# rubocop:disable Security/MarshalLoad
Marshal.load(File.read(@file)) if File.exist?(@file)
# rubocop:enable Security/MarshalLoad
end
def save(weights)
File.write(@file, Marshal.dump(weights))
end
end
================================================
FILE: model/linear.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'json'
require 'time'
require 'crack'
require_relative 'predictor'
require_relative 'storage'
require_relative 'fake_weights_storage'
#
# Linear Model
# @todo #532:60min Add unit-tests.
# We should add unit-tests for this class that checks puzzle ranking.
# For now its untested, don't forget to remove this puzzle.
#
class LinearModel
def initialize(repo, storage)
@repo = repo
@xml_storage = storage
if ENV['RACK_ENV'] == 'test'
@storage = FakeWeightsStorage.new(@repo)
else
settings = Sinatra::Application.settings
@storage = Storage.new(
"#{@repo}.marshal",
settings.config['s3']['bucket'],
settings.config['s3']['region'],
settings.config['s3']['key'],
settings.config['s3']['secret']
)
end
end
# ranks the puzzles using Machine-Learning
# @param puzzles XML puzzles
# @return array of positional index of the input puzzles
# @todo #532:60min Implement a ranked puzzles.
# Let's implement a class that will use `LinearModel` to rank puzzles.
# This class is need in order to do an integration between original 0pdd
# and model modules. Probably it can be a decorator for `Puzzles`
# that ranks XML puzzles, and then submits them into `Puzzles`.
# Don't forget to remove this puzzle.
def predict(puzzles)
weights = @storage.load # load weights for repo from s3
clf = Predictor.new(
layers: [
{ name: 'w1', shape: [10, 1] },
{ name: 'w2', shape: [1, 1] }
]
)
if weights.nil?
train(clf) # find weights for repo backlog of puzzles
ranks = naive_rank(puzzles) # naive rank of puzzles in each repo
else
# get x and y data for puzzles
samples, _labels = extract_features(puzzles)
ranks = clf.predict(weights, samples[0]) # model rank of puzzles if weights are loaded
end
ranks.map(&:to_i)
end
private
def replace_nil(arr, with = 0)
arr.map { |x| x.nil? ? with : x }
end
def get_features_labels(samples)
x = samples.map do |_, s|
replace_nil([
s['time_estimate'],
s['n_characters'],
s['level'],
s['n_puzzles_before'],
s['n_puzzles_after'],
s['time_before'],
s['time_after'],
s['n_additions'],
s['n_deletions']
].append(s['vectorized_description']))
end
y = samples.map { |_, s| s['closed'] ? Time.parse(s['closed']).to_i : 0 }.map.with_index.sort.map(&:last)
[[x], [y]] # single backlog of puzzles
end
# depth first feature extraction
def extract_features(puzzles, samples = {}, level = 1)
puzzles = [puzzles] unless puzzles.is_a?(Array)
puzzles.each do |puzzle|
next if puzzle.nil?
prev_puzzle = samples[samples.keys.last]
time_before = 0
unless prev_puzzle.nil?
opened = Time.parse(prev_puzzle['time']).to_i
closed = prev_puzzle['closed'] ? Time.parse(prev_puzzle['closed']).to_i : opened
time_before = (closed - opened) / 60 # in minutes
unless prev_puzzle['time_after'].nil?
time_after = (Time.parse(puzzle['closed']).to_i - Time.parse(puzzle['time']).to_i) / 60 # in minutes
prev_puzzle['time_after'] = time_after
end
end
n_characters = puzzle['body'].gsub(/\s/, '').length
samples[puzzle['id']] = {
'time_estimate' => puzzle['estimate'].to_i,
'n_characters' => n_characters,
'level' => level,
'n_puzzles_before' => samples.length,
'n_puzzles_after' => puzzles.length - samples.length,
'time_before' => time_before
}.merge(puzzle)
extract_features(puzzle['children']['puzzle'], samples, level + 1) unless puzzle['children'].nil?
end
get_features_labels(samples) if level == 1
end
def train(clf)
puzzles = @xml_storage.load
Thread.new do
# properly train model here and save weights to s3 for later
puzzles = JSON.parse(Crack::XML.parse(puzzles.to_s).to_json)['puzzles']
unless puzzles.nil?
samples, labels = extract_features(puzzles['puzzle'])
if labels[0].length > 1 # train only when there's data
center = ZeroVector.zero(samples[0][0].size)
solver = Pso::Solver.new(f: clf, center: center, data: samples, true_order: labels)
_rank, weights, _n_iterations = solver.solve
@storage.save(weights)
end
end
end
end
def naive_rank(puzzles)
estimates = puzzles.map { |puzzle| puzzle['estimate'].to_i }
estimates.map.with_index.sort.map(&:last)
end
end
================================================
FILE: model/predictor.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'pso/pso'
def argsort(arr)
arr.map.with_index.sort.map(&:last)
end
def normalised_kendall_tau_distance(a, b)
raise 'Both lists have to be of equal length' unless a.size == b.size
a = argsort(a)
b = argsort(b)
combination = a.combination(2)
disordered = 0
combination.each do |i, j|
is_disordered = (a[i] > a[j] && b[i] < b[j]) || (a[i] < a[j] && b[i] > b[j])
disordered += 1 if is_disordered
end
n = a.size
(2.0 * disordered.to_f) / (n * (n - 1.0))
end
def default_option_generator_linear(attribute_num)
[
{ layers: [{ name: 'w1', shape: [attribute_num, 1] }, { name: 'w2', shape: [1, 1] }] },
[attribute_num] + 1
]
end
#
# Linear Predictor Model
#
class Predictor
def initialize(**options)
@layers = {}
@kendall_corr_history = []
options[:layers].each do |layer|
@layers["#{layer[:name]}_shape"] = layer[:shape]
end
end
def f(weights, **options)
data = options[:data]
true_order = options[:true_order]
kns = []
(0...data.size).each do |i|
x = data[i]
y = true_order[i].first(x.size)
preds = predict(weights, x)
kn = normalised_kendall_tau_distance(preds, y)
kns.append(kn)
end
kns.sum / kns.size # mean
end
def train(weights, data, true_order)
ranks = predict(weights, data)
normalised_kendall_tau_distance(ranks, true_order)
end
def predict(weights, data)
ranks = []
(0...data.size).each do |i|
row = data[i]
r = forward_one(weights, row)
ranks.append(r)
end
ranks
end
def forward_one(weights, data)
x = data.clone.map(&:clone).flatten
w = weights.first(x.size)
x = Vector[*x].dot(Vector[*w])
w.map { |c| x += c }[0]
end
def kendall(weights, data, true_order)
x = predict(weights, data)
normalised_kendall_tau_distance(x, true_order)
end
end
================================================
FILE: model/pso/lib/function.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'matrix'
module Pso
#
# General Objective Function Interface
#
class Function
def f(vector, **_options)
vector.magnitude
end
end
end
================================================
FILE: model/pso/lib/functions/rastrigin.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative '../function'
require_relative '../zero_vector'
module Pso
#
# Rastrigin Objective Function
#
class Rastrigin < Pso::Function
def f(vector, **_options)
fitness = 10 * vector.size
fitness + vector.sum { |n| (n**2) - (10 * Math.cos(2 * Math::PI * n)) }
end
end
end
================================================
FILE: model/pso/lib/functions/schwefel.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative '../function'
require_relative '../zero_vector'
module Pso
#
# Schwefel Objective Function
#
class Schwefel < Pso::Function
def f(vector, **_options)
alpha = 418.982887
vector.sum { |n| -n * Math.sin(Math.sqrt(n.to_f.abs)) } + (alpha * vector.size)
end
end
end
================================================
FILE: model/pso/lib/solver.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'zero_vector'
require_relative 'functions/rastrigin'
# rubocop:disable Metrics/ParameterLists
module Pso
#
# PSO Solver
#
class Solver
def initialize(
din: 5,
density: 50,
f: Pso::Rastrigin,
center: nil,
radius: 5.12,
method: :min_by,
**options
)
begin
@f = f.new
rescue NoMethodError
@f = f
end
@din = din
@center = center
@radius = radius
@method = method
@density = density
@options = options
generate_swarm
end
def generate_swarm
Array.new(@density)
@swarm = Array.new(@density) { generate_random_particle }
@swarm_best = @swarm.map { |particle| [@f.f(particle, **@options), particle] }
@swarm_speed = @swarm.map { generate_random_particle }
end
def generate_random_noise_particle
@center.map { (rand * 2) - 1 }
end
def generate_random_particle
@center + (generate_random_noise_particle * (@radius * rand))
end
def perfect_particle
if @method == :min_by
@swarm.min_by do |element|
@f.f(element, **@options)
end
else
@swarm.max_by do |element|
@f.f(element, **@options)
end
end
end
def solve(precision: 100, threads: 1, debug: false)
n_iterations = 0
Array.new(threads).map do
Thread.new do
((precision / @swarm.size) / threads).times do |_|
n_iterations += 1
(0...@density).each do |index|
perfect = perfect_particle
puts @f.f(perfect, **@options) if debug
new_vector = normalize(iterate(@swarm[index], @swarm_best[index].last, perfect, @swarm_speed[index]))
if best?(@swarm_best[index].first, @f.f(new_vector, **@options))
@swarm_best[index] = [@f.f(new_vector, **@options), new_vector]
end
@swarm_speed[index] = (new_vector - @swarm[index]).normalize
@swarm[index] = new_vector
end
end
end
end.each(&:join)
perfect = perfect_particle
[@f.f(perfect, **@options), perfect, n_iterations]
end
private
def best?(best, now)
if @method == :min_by
now < best
else
now > best
end
end
def normalize(vector)
return ((vector - @center).normalize * @radius) + @center if (vector - @center).magnitude > @radius
vector
end
def iterate(vector, best, perfect, speed)
if vector == perfect
out = generate_random_noise_particle
new_vec = vector + ((best - vector).normalize * 0.2) + (out * rand * 0.05) + (speed * 0.05)
minimal = @f.f(vector, **@options) > @f.f(new_vec, **@options)
return minimal ? new_vec : vector if @method == :min_by
return minimal ? vector : new_vec unless @method == :min_by
end
out = generate_random_noise_particle
vector + (out * rand * 0.1) + ((best - vector).normalize * 0.5) + (perfect - vector).normalize + speed
end
end
end
# rubocop:enable Metrics/ParameterLists
================================================
FILE: model/pso/lib/version.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
module Pso
VERSION = '0.1.1'.freeze
end
================================================
FILE: model/pso/lib/zero_vector.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'matrix'
#
# Zero vector class
#
class ZeroVector < Vector
def normalize
return self if zero?
super
end
end
================================================
FILE: model/pso/pso.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'lib/version'
require_relative 'lib/solver'
#
# PSO Module
#
module Pso
end
================================================
FILE: model/storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'json'
require 'aws-sdk-s3'
require_relative '../version'
#
# S3 storage.
#
class Storage
def initialize(ocket, bucket, region, key, secret)
@object = Aws::S3::Resource.new(
region: region,
credentials: Aws::Credentials.new(key, secret)
).bucket(bucket).object(ocket)
end
def load
return unless @object.exists?
data = @object.get.body
puts "S3 #{data.size} from #{@object.bucket_name}/#{@object.key}"
# rubocop:disable Security/MarshalLoad
Marshal.load(data)
# rubocop:enable Security/MarshalLoad
end
def save(weights)
data = Marshal.dump(weights)
@object.put(body: data)
puts "S3 #{data.size} to #{@object.bucket_name}/#{@object.key}"
end
end
================================================
FILE: nginx.conf.sigil
================================================
{{ range $port_map := .PROXY_PORT_MAP | split " " }}
{{ $port_map_list := $port_map | split ":" }}
{{ $scheme := index $port_map_list 0 }}
{{ $listen_port := index $port_map_list 1 }}
{{ $upstream_port := index $port_map_list 2 }}
{{ if eq $scheme "http" }}
server {
listen [::]:{{ $listen_port }};
listen {{ $listen_port }};
{{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }}
access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log;
error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log;
location / {
gzip on;
gzip_min_length 1100;
gzip_buffers 4 32k;
gzip_types text/css text/javascript text/xml text/plain text/x-component application/javascript application/x-javascript application/json application/xml application/rss+xml font/truetype application/x-font-ttf font/opentype application/vnd.ms-fontobject image/svg+xml;
gzip_vary on;
gzip_comp_level 6;
proxy_pass http://{{ $.APP }}-{{ $upstream_port }};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Request-Start $msec;
}
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
error_page 400 401 402 403 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 426 428 429 431 444 449 450 451 /400-error.html;
location /400-error.html {
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
internal;
}
error_page 404 /404-error.html;
location /404-error.html {
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
internal;
}
error_page 500 501 502 503 504 505 506 507 508 509 510 511 /500-error.html;
location /500-error.html {
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
internal;
}
}
{{ else if eq $scheme "https"}}
server {
listen [::]:{{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }};
listen {{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }};
{{ if $.SSL_SERVER_NAME }}server_name {{ $.SSL_SERVER_NAME }}; {{ end }}
{{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }}
access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log;
error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log;
ssl_certificate {{ $.APP_SSL_PATH }}/server.crt;
ssl_certificate_key {{ $.APP_SSL_PATH }}/server.key;
ssl_protocols TLSv1.2 {{ if eq $.TLS13_SUPPORTED "true" }}TLSv1.3{{ end }};
ssl_prefer_server_ciphers off;
keepalive_timeout 70;
{{ if and (eq $.SPDY_SUPPORTED "true") (ne $.HTTP2_SUPPORTED "true") }}add_header Alternate-Protocol {{ $.PROXY_SSL_PORT }}:npn-spdy/2;{{ end }}
location / {
gzip on;
gzip_min_length 1100;
gzip_buffers 4 32k;
gzip_types text/css text/javascript text/xml text/plain text/x-component application/javascript application/x-javascript application/json application/xml application/rss+xml font/truetype application/x-font-ttf font/opentype application/vnd.ms-fontobject image/svg+xml;
gzip_vary on;
gzip_comp_level 6;
proxy_pass http://{{ $.APP }}-{{ $upstream_port }};
{{ if eq $.HTTP2_PUSH_SUPPORTED "true" }}http2_push_preload on; {{ end }}
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Request-Start $msec;
}
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
error_page 400 401 402 403 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 426 428 429 431 444 449 450 451 /400-error.html;
location /400-error.html {
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
internal;
}
error_page 404 /404-error.html;
location /404-error.html {
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
internal;
}
error_page 500 501 503 504 505 506 507 508 509 510 511 /500-error.html;
location /500-error.html {
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
internal;
}
error_page 502 /502-error.html;
location /502-error.html {
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
internal;
}
}
{{ else if eq $scheme "grpc"}}
{{ if eq $.GRPC_SUPPORTED "true"}}{{ if eq $.HTTP2_SUPPORTED "true"}}
server {
listen [::]:{{ $listen_port }} http2;
listen {{ $listen_port }} http2;
{{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }}
access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log;
error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log;
location / {
grpc_pass grpc://{{ $.APP }}-{{ $upstream_port }};
}
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
}
{{ end }}{{ end }}
{{ else if eq $scheme "grpcs"}}
{{ if eq $.GRPC_SUPPORTED "true"}}{{ if eq $.HTTP2_SUPPORTED "true"}}
server {
listen [::]:{{ $listen_port }} ssl http2;
listen {{ $listen_port }} ssl http2;
{{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }}
access_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-access.log;
error_log {{ $.NGINX_LOG_ROOT }}/{{ $.APP }}-error.log;
ssl_certificate {{ $.APP_SSL_PATH }}/server.crt;
ssl_certificate_key {{ $.APP_SSL_PATH }}/server.key;
ssl_protocols TLSv1.2 {{ if eq $.TLS13_SUPPORTED "true" }}TLSv1.3{{ end }};
ssl_prefer_server_ciphers off;
location / {
grpc_pass grpc://{{ $.APP }}-{{ $upstream_port }};
}
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
}
{{ end }}{{ end }}
{{ end }}
{{ end }}
{{ if $.DOKKU_APP_LISTENERS }}
{{ range $upstream_port := $.PROXY_UPSTREAM_PORTS | split " " }}
upstream {{ $.APP }}-{{ $upstream_port }} {
{{ range $listeners := $.DOKKU_APP_LISTENERS | split " " }}
{{ $listener_list := $listeners | split ":" }}
{{ $listener_ip := index $listener_list 0 }}
server {{ $listener_ip }}:{{ $upstream_port }};{{ end }}
}
{{ end }}{{ end }}
================================================
FILE: objects/clients/github.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'octokit'
#
# Github client
# API: http://octokit.github.io/octokit.rb/method_list.html
#
class Github
def initialize(config = {})
@config = config
end
def client
if @config['testing']
require_relative '../../test/fake_github'
FakeGithub.new
else
args = {}
args[:access_token] = @config['github']['token'] if @config['github']
Octokit.connection_options = {
request: {
timeout: 20,
open_timeout: 20
}
}
Octokit.auto_paginate = true
Octokit::Client.new(args)
end
end
end
================================================
FILE: objects/clients/gitlab.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'gitlab'
#
# Gitlab client
# API: https://github.com/NARKOZ/gitlab
#
class GitlabClient
def initialize(config = {})
@config = config
end
def client
if @config['testing']
require_relative '../../test/fake_gitlab'
FakeGitlab.new
else
token = @config['gitlab']['token'] if @config['gitlab']
Gitlab.client(
endpoint: 'https://gitlab.com/api/v4',
private_token: token,
httparty: {
headers: { 'Cookie' => 'gitlab_canary=true' }
}
)
end
end
end
================================================
FILE: objects/clients/jira.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'rubygems'
require 'jira-ruby'
#
# Jira client
# API: https://github.com/sumoheavy/jira-ruby
#
class JiraClient
def initialize(config = {})
@config = config
end
def client
if @config['testing']
# require_relative '../../test/fake_jira'
# FakeJira.new
else
username = @config['jira']['username'] if @config['jira']
token = @config['jira']['token'] if @config['jira']
options = {
username: username,
password: token,
site: 'http://localhost:8080/', # or 'https://<your_subdomain>.atlassian.net/' # often blank
auth_type: :basic,
read_timeout: 120
}
JIRA::Client.new(options)
end
end
end
================================================
FILE: objects/diff.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
#
# Diff.
#
class Diff
def initialize(before, after)
@before = before
@after = after
end
def notify(tickets)
@after.xpath('//puzzle/ticket/text()').map(&:to_s).uniq.each do |t|
current = summary(@after, t)
previous = summary(@before, t)
next if previous == current
next if current.empty?
tickets.notify(t, "#{current}.")
end
end
private
def issues(xml, *xpath)
xpath.map { |x| xml.xpath(x) }.flatten.map do |p|
issue = p.xpath('issue')
if issue.empty?
"`#{p.xpath('id')}`"
else
number = issue[0].text
link = issue[0]['href']
number = link.split('/')[-1] if link && number == 'unknown'
"[##{number}](#{link})"
end
end.sort
end
def summary(xml, ticket)
all = issues(
xml,
"//puzzle[ticket='#{ticket}']/children//puzzle",
"//puzzle[ticket='#{ticket}']"
)
alive = issues(
xml,
"//puzzle[ticket='#{ticket}']/children//puzzle[@alive='true']",
"//puzzle[ticket='#{ticket}' and @alive='true']"
)
if alive.empty?
if all.empty?
''
elsif all.length == 1
"the only puzzle #{all[0]} is solved here"
else
"all #{all.length} puzzles are solved here: #{all.join(', ')}"
end
else
solved = all - alive
tail = solved.empty? ? '' : "; solved: #{solved.join(', ')}"
if alive.length == 1
"the puzzle #{alive[0]} is still not solved"
else
"#{alive.length} puzzles #{alive.join(', ')} are still not solved"
end + tail
end
end
end
================================================
FILE: objects/dynamo.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'yaml'
require 'aws-sdk-dynamodb'
#
# Dynamo client
#
class Dynamo
def initialize(config = {})
@config = config
end
def aws
Aws::DynamoDB::Client.new(
if ENV['RACK_ENV'] == 'test'
cfg = File.join(Dir.pwd, 'dynamodb-local/target/dynamo.yml')
raise 'Test config is absent' unless File.exist?(cfg)
yaml = YAML.safe_load(File.open(cfg))
{
region: 'us-east-1',
endpoint: "http://localhost:#{yaml['port']}",
access_key_id: yaml['key'],
secret_access_key: yaml['secret'],
http_open_timeout: 5,
http_read_timeout: 5
}
else
{
region: @config['dynamo']['region'],
access_key_id: @config['dynamo']['key'],
secret_access_key: @config['dynamo']['secret']
}
end
)
end
end
================================================
FILE: objects/git_repo.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'base64'
require 'fileutils'
require 'pdd'
require 'qbash'
require 'shellwords'
require 'tempfile'
require 'tmpdir'
require 'yaml'
require_relative 'user_error'
#
# Repository in Git
#
class GitRepo
attr_reader :uri, :name, :path, :master, :head_commit_hash, :target
def initialize(
uri:,
name:,
master: 'master',
head_commit_hash: '',
**options
)
@id = Base64.encode64(uri).gsub(%r{[\s=/]+}, '')
@name = name
@dir = options[:dir] || Dir.mktmpdir('0pdd')
@path = "#{@dir}/#{@id}"
@uri = uri
@id_rsa = options[:id_rsa] || ''
@master = master
@head_commit_hash = head_commit_hash
@target = options[:target] || 'master'
end
def lock
"/tmp/0pdd-locks/#{@id}.txt"
end
def config
f = File.join(@path, '.0pdd.yml')
if File.exist?(f)
YAML.safe_load(File.open(f))
else
{}
end
end
def xml
raise "Path is absent: #{@path}" unless File.exist?(@path)
Tempfile.open do |f|
begin
qbash("cd #{Shellwords.escape(@path)} && pdd -v -f #{Shellwords.escape(f.path)}")
rescue StandardError => e
raise UserError, e.message
end
Nokogiri::XML(File.read(f))
end
end
def push
if File.exist?(@path)
pull
else
clone
end
end
def change_in_master?
"refs/heads/#{master}".eql?(target)
end
private
def clone
prepare_key
prepare_git
qbash(['git clone', '--depth=1', '--quiet', Shellwords.escape(@uri), Shellwords.escape(@path)])
end
def pull
prepare_key
prepare_git
qbash(
[
"cd #{Shellwords.escape(@path)}",
"master=#{Shellwords.escape(@master)}",
'git config --local core.autocrlf false',
'git reset origin/${master} --hard --quiet',
'git clean --force -d',
'git fetch --quiet',
'git checkout origin/${master}',
'git rebase --abort || true',
'git rebase --autostash --strategy-option=theirs origin/${master}'
].join(' && ')
)
end
def prepare_key
dir = "#{Dir.home}/.ssh"
return if File.exist?(dir)
FileUtils.mkdir_p(dir)
File.write("#{dir}/id_rsa", @id_rsa) unless @id_rsa.empty?
qbash(
[
'echo "Host *" > ~/.ssh/config',
'echo " StrictHostKeyChecking no" >> ~/.ssh/config',
'echo " UserKnownHostsFile=~/.ssh/known_hosts" >> ~/.ssh/config',
'chmod -R 600 ~/.ssh/*'
].join(';')
)
end
def prepare_git
qbash(
[
'GIT=$(git --version)',
'if [[ "${GIT}" != "git version 2."* ]]',
'then echo "Git is too old: ${GIT}"',
'exit -1',
'fi'
].join(';')
)
return if ENV['RACK_ENV'] == 'test'
qbash(
[
'if ! git config --get --global user.email',
'then git config --global user.email "server@0pdd.com"',
'fi',
'if ! git config --get --global user.name',
'then git config --global user.name "0pdd.com"',
'fi'
].join(';')
)
end
end
================================================
FILE: objects/invitations/github_invitations.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Invitations in Github
#
class GithubInvitations
def initialize(github)
@github = github
end
def accept
@github.user_repository_invitations.each do |i|
break if @github.rate_limit.remaining < 1000
puts "Repository invitation #{i['id']} accepted" if @github.accept_repository_invitation(i['id'])
end
end
def accept_single_invitation(repo)
invitations = @github.user_repository_invitations(repo: repo)
invitations.map do |i|
break if @github.rate_limit.remaining < 1000
"Repository invitation #{repo} accepted" if @github.accept_repository_invitation(i['id'])
end
end
def accept_orgs
@github.organization_memberships('state' => 'pending').each do |m|
break if @github.rate_limit.remaining < 1000
org = m['organization']['login']
begin
@github.update_organization_membership(org, 'state' => 'active')
puts "Invitation for @#{org} accepted"
rescue Octokit::NotFound
# puts "Failed to join @#{org} organization: #{e.message}"
@github.remove_organization_membership(org)
# puts "Membership in @#{org} organization removed"
end
end
end
end
================================================
FILE: objects/invitations/github_organization_invitations.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'github_organization_invitation'
#
# Invitations to join Github organizations
#
class GithubOrganizationInvitations
def initialize(github)
@github = github
end
def all
@github.organization_memberships(state: 'pending').collect do |membership|
GithubOrganizationInvitation.new(membership, @github)
end
end
end
================================================
FILE: objects/jobs/job.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'mail'
require_relative '../diff'
require_relative '../puzzles'
#
# One job.
#
class Job
def initialize(vcs, storage, tickets)
@vcs = vcs
@storage = storage
@tickets = tickets
end
def proceed
@vcs.repo.push
before = @storage.load
Puzzles.new(@vcs.repo, @storage).deploy(@tickets)
return if opts.include?('on-scope')
Diff.new(before, @storage.load).notify(@tickets)
end
private
def opts
array = @vcs.repo.config.dig('alerts', 'suppress')
array.nil? || !array.is_a?(Array) ? [] : array
end
end
================================================
FILE: objects/jobs/job_commiterrors.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative '../truncated'
#
# Job that posts exceptions as commit messages.
#
class JobCommitErrors
def initialize(vcs, job)
@vcs = vcs
@job = job
end
def proceed
@job.proceed
rescue Exception => e
done = @vcs.create_commit_comment(
@vcs.repo.head_commit_hash,
"I wasn't able to retrieve PDD puzzles from the code base and \
submit them to #{@vcs.name}. If you \
think that it's a bug on our side, please submit it to \
[yegor256/0pdd](https://github.com/yegor256/0pdd/issues):\n\n\
> #{Truncated.new(e.message.gsub(/\s/, ' '), 300)}\n\n
Please, copy and paste this stack trace to GitHub:\n\n
```\n#{e.class.name}\n#{e.message}\n#{e.backtrace.join("\n")}\n```"
)
puts "Comment posted about an error: #{done[:html_url]}"
raise e
end
end
================================================
FILE: objects/jobs/job_detached.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'fileutils'
#
# One job.
#
class JobDetached
def initialize(vcs, job)
@vcs = vcs
@job = job
end
def proceed
if ENV['RACK_ENV'] == 'test'
exclusive
else
Process.detach(fork { exclusive })
end
end
private
def exclusive
lock = @vcs.repo.lock
FileUtils.mkdir_p(File.dirname(lock))
f = File.open(lock, File::RDWR | File::CREAT, 0o644)
f.flock(File::LOCK_EX)
begin
@job.proceed
ensure
f.close
begin
File.delete(lock)
rescue Errno::EACCES
lock.close
File.delete(lock)
end
end
end
end
================================================
FILE: objects/jobs/job_emailed.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'mail'
#
# Job that emails if exception occurs.
#
class JobEmailed
def initialize(vcs, job)
@vcs = vcs
@job = job
end
def proceed
@job.proceed
rescue Exception => e
yaml = @vcs.repo.config
emails = yaml['errors'] || []
emails << 'admin@0pdd.com'
trace = "#{e.message}\n\n#{e.backtrace.join("\n")}"
name = @vcs.repo.name
repo_owner_login = repo_user_login
repo_owner_email = user_email(repo_owner_login)
repository_link = @vcs.repository_link
emails.each do |email|
mail = Mail.new do
from '0pdd <no-reply@0pdd.com>'
to email
subject "#{name}: puzzles discovery problem"
text_part do
content_type 'text/plain; charset=UTF-8'
body "Hey,\n\n\
There is a problem in #{repository_link}:\n\n\
#{trace}\n\n\
If you think it's our bug, please submit it to GitHub: \
https://github.com/yegor256/0pdd/issues\n\n\
Sorry,\n\
0pdd"
end
html_part do
content_type 'text/html; charset=UTF-8'
body "<html><body><p>Hey,</p>
<p>There is a problem in
<a href='#{repository_link}'>#{name}</a>:</p>
<pre>#{trace}</pre>
<p>If you think it's our bug, please submit it to
<a href='https://github.com/yegor256/0pdd/issues'>GitHub</a>.
Thanks.</p>
<p>Sorry,<br/><a href='https://www.0pdd.com'>0pdd</a></p>"
end
end
mail.cc = repo_owner_email if repo_owner_email
mail.deliver!
puts "Email sent to #{email}"
end
raise e
end
private
def repo_user_login
@vcs.repo.name.split('/').first
end
def user_email(username)
@vcs.user(username)[:email]
end
end
================================================
FILE: objects/jobs/job_recorded.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Job that records all requests.
#
class JobRecorded
def initialize(vcs, job)
@vcs = vcs
@job = job
end
def proceed
@job.proceed
open('/tmp/0pdd-done.txt', 'a+') do |f|
f.puts(@vcs.repo.name)
end
end
end
================================================
FILE: objects/jobs/job_starred.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Job that stars the repo.
# API: http://octokit.github.io/octokit.rb/method_list.html
#
class JobStarred
def initialize(vcs, job)
@vcs = vcs
@job = job
end
def proceed
output = @job.proceed
@vcs.star
output
end
end
================================================
FILE: objects/log.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'base64'
require 'nokogiri'
require 'aws-sdk-dynamodb'
require_relative 'dynamo'
require_relative '../version'
#
# Log.
#
class Log
def initialize(dynamo, repo, vcs = 'github')
@dynamo = dynamo
# @todo #312:30min Be sure to handle the use case where projects from
# different vcs have the same <user/repo_name>. This will cause a conflict.
@vcs = (vcs || 'github').downcase
@repo = @vcs == 'github' ? repo : Base64.encode64(repo + @vcs).gsub(%r{[\s=/]+}, '')
raise 'You need to specify your cloud VCS' unless ['github'].include?(@vcs)
end
def put(tag, text)
@dynamo.put_item(
table_name: '0pdd-events',
item: {
'repo' => @repo,
'vcs' => @vcs,
'time' => Time.now.to_i,
'tag' => tag,
'text' => "#{text} /#{VERSION}"
}
)
end
def get(tag)
@dynamo.query(
table_name: '0pdd-events',
index_name: 'tags',
select: 'ALL_ATTRIBUTES',
limit: 1,
expression_attribute_values: {
':r' => @repo,
':t' => tag
},
key_condition_expression: 'repo=:r and tag=:t'
).items[0]
end
def exists(tag)
!@dynamo.query(
table_name: '0pdd-events',
index_name: 'tags',
select: 'ALL_ATTRIBUTES',
limit: 1,
expression_attribute_values: {
':r' => @repo,
':t' => tag
},
key_condition_expression: 'repo=:r and tag=:t'
).items.empty?
end
def delete(time, tag)
@dynamo.delete_item(
table_name: '0pdd-events',
key: {
'repo' => @repo,
'time' => time
},
expression_attribute_values: {
':t' => tag
},
condition_expression: 'tag=:t'
)
end
def list(since = Time.now.to_i)
@dynamo.query(
table_name: '0pdd-events',
select: 'ALL_ATTRIBUTES',
limit: 25,
scan_index_forward: false,
expression_attribute_names: {
'#time' => 'time'
},
expression_attribute_values: {
':r' => @repo,
':t' => since
},
key_condition_expression: 'repo=:r and #time<:t'
)
end
end
================================================
FILE: objects/maybe_text.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Maybe text
#
class MaybeText
def initialize(text_if_present, maybe, exclude_if: false)
@maybe = maybe
@text = text_if_present
@exclude_if = exclude_if
end
def to_s
if @maybe.nil? || @maybe.empty? || @maybe == @exclude_if
''
else
@text
end
end
end
================================================
FILE: objects/puzzles.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'json'
require 'crack'
require 'nokogiri'
require_relative '../model/linear'
#
# Puzzles in XML/S3
# @todo #532:60min Implement a decorator for optional model configuration load.
# Let's implement a class that decorates `Puzzles` and
# based on presence of `model: true` attribute in YAML config, decides
# whether the puzzles should be ranked or not.
# Don't forget to remove this puzzle.
#
class Puzzles
def initialize(repo, storage)
@repo = repo
@storage = storage
t = repo.config && repo.config['threshold'].to_i
@threshold = t.positive? && t < 256 ? t : 256
end
# Find out which puzzles deservers to become new tickets and submit
# them to the repository (GitHub, for example). Also, find out which
# puzzles are no longer active and remove them from GitHub.
def deploy(tickets)
xml = join(@storage.load, @repo.xml)
xml = group(xml)
save(xml)
expose(xml, tickets)
end
private
# Save new XML into the storage, replacing the existing one.
def save(xml)
@storage.save(xml)
end
# Join existing XML with the snapshot just arrived from PDD
# toolkit output after the analysis of the code base. New <puzzle>
# elements are added as <extra> elements. They later inside the
# method join() will be placed to the right positions and will
# either replace existing ones of will become new puzzles.
def join(before, snapshot)
after = Nokogiri::XML(before.to_s)
target = after.xpath('/puzzles')[0]
snapshot.xpath('//puzzle').each do |p|
p.name = 'extra'
target.add_child(p)
end
after
end
# Merge <extra> elements with <puzzle> elements in the XML. Some
# extras will be simply deleted, while others will become new
# puzzles.
def group(xml)
Nokogiri::XSLT(File.read('assets/xsl/group.xsl')).transform(
Nokogiri::XSLT(File.read('assets/xsl/join.xsl')).transform(xml)
)
end
# Take some puzzles from the XML and either close their tickets in GitHub
# or create new tickets.
def expose(xml, tickets)
seen = []
Kernel.loop do
puzzles = xml.xpath(
[
'//puzzle[@alive="false" and issue',
'and issue != "unknown" and not(issue/@closed)',
seen.map { |i| "and id != '#{i}'" }.join(' '),
']'
].join(' ')
)
break if puzzles.empty?
puzzle = puzzles[0]
puzzle.search('issue')[0]['closed'] = Time.now.iso8601 if tickets.close(puzzle)
save(xml)
end
seen = []
Kernel.loop do
puzzles = xml.xpath(
[
'//puzzle[@alive="true" and (not(issue) or issue="unknown")',
seen.map { |i| "and id != '#{i}'" }.join(' '),
']'
].join(' ')
)
break if puzzles.empty?
puzzle = puzzles[0]
id = puzzle.xpath('id')[0].text
seen << id
issue = tickets.submit(puzzle)
next if issue.nil?
puzzle.search('issue').remove
puzzle.add_child(
"<issue href='#{issue[:href]}'>#{issue[:number]}</issue>"
)
save(xml)
end
end
end
================================================
FILE: objects/storage/cached_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# XML cached in a temporary file.
#
class CachedStorage
def initialize(origin, file)
@origin = origin
@file = file
end
def load
if File.exist?(@file)
begin
content = File.read(@file)
rescue StandardError => e
raise "Failed to read #{@file} due to #{e.cause.inspect}"
end
xml = Nokogiri::XML(content)
else
xml = @origin.load
write(xml)
end
xml
end
def save(xml)
FileUtils.rm_rf(@file)
@origin.save(xml)
write(xml.to_s)
end
private
def write(xml)
FileUtils.mkdir_p(File.dirname(@file))
File.write(@file, xml)
end
end
================================================
FILE: objects/storage/logged_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Storage that is logged.
#
class LoggedStorage
def initialize(origin, log)
@origin = origin
@log = log
end
def load
@origin.load
end
def save(xml)
@origin.save(xml)
@log.put(
"save-#{Time.now.to_i}",
"Saved XML, puzzles:#{xml.xpath('//puzzle[@alive="true"]').size}/\
#{xml.xpath('//puzzle').size}, chars:#{xml.to_s.length}, \
date:#{xml.xpath('/*/@date')[0].text}, \
version:#{xml.xpath('/*/@version')[0].text}"
)
end
end
================================================
FILE: objects/storage/once_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Saves only once, if the content wasn't really changed.
#
class OnceStorage
def initialize(origin)
@origin = origin
end
def load
@origin.load
end
def save(xml)
@origin.save(xml) if load.to_s != xml.to_s
end
end
================================================
FILE: objects/storage/s3.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'aws-sdk-s3'
require 'nokogiri'
require_relative '../../version'
#
# S3 storage.
#
class S3
def initialize(ocket, bucket, region, key, secret)
@object = Aws::S3::Resource.new(
region: region,
credentials: Aws::Credentials.new(key, secret)
).bucket(bucket).object(ocket)
end
def load
Nokogiri::XML(
if @object.exists?
data = @object.get.body
puts "S3 #{data.size} from #{@object.bucket_name}/#{@object.key}"
data
else
puts "Empty puzzles for #{@object.bucket_name}/#{@object.key}"
'<puzzles xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://www.0pdd.com/puzzles.xsd"/>'
end
)
end
def save(xml)
data = xml.to_s
@object.put(body: data)
puts "S3 #{data.size} to #{@object.bucket_name}/#{@object.key} \
(#{xml.xpath('//puzzle').size} puzzles)"
end
end
================================================
FILE: objects/storage/safe_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
#
# Safe, XSD validated, storage.
#
class SafeStorage
def initialize(origin)
@origin = origin
@xsd = Nokogiri::XML::Schema(File.read('assets/xsd/puzzles.xsd'))
end
def load
@origin.load
end
def save(xml)
@origin.save(valid(xml))
end
private
def valid(xml)
errors = @xsd.validate(xml).each(&:message)
raise "XML has #{errors.length} errors\nw#{errors.join("\n")}\n#{xml}" unless errors.empty?
xml
end
end
================================================
FILE: objects/storage/sync_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Thread-safe storage.
#
class SyncStorage
def initialize(origin)
@origin = origin
@mutex = Mutex.new
end
def load
@mutex.synchronize { @origin.load }
end
def save(xml)
@mutex.synchronize { @origin.save(xml) }
end
end
================================================
FILE: objects/storage/upgraded_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Storage that upgrades itself on load.
#
class UpgradedStorage
def initialize(origin, version)
@origin = origin
@version = version
end
def load
xml = @origin.load
if xml.xpath('/*/@version')[0] != @version
%w[remove-broken-issues add-namespace].each do |xsl|
xml = Nokogiri::XSLT(
File.read("assets/upgrades/#{xsl}.xsl")
).transform(xml)
end
save(xml)
end
xml
end
def save(xml)
@origin.save(xml)
end
end
================================================
FILE: objects/storage/versioned_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Storage that adds version to the XML when it gets saved.
#
class VersionedStorage
def initialize(origin, version)
@origin = origin
@version = version
end
def load
xml = @origin.load
root = xml.xpath('/*')[0]
unless root['date']
root['date'] = '2016-12-08T12:00:49Z'
root['version'] = '0.0.0'
end
xml
end
def save(xml)
root = xml.xpath('/*')[0]
root['date'] = Time.now.iso8601
root['version'] = @version
@origin.save(xml)
end
end
================================================
FILE: objects/templates/github_tickets_body.haml
================================================
-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
-# SPDX-License-Identifier: MIT
The puzzle `#{puzzle.xpath('id')[0].text}` |
from ##{puzzle.xpath('ticket')[0].text} has to be resolved: |
\
#{url}
\
The puzzle was created by #{puzzle.xpath('author')[0].text} on |
#{Time.parse(puzzle.xpath('time')[0].text).strftime('%d-%b-%y')}. |
\
#{MaybeText.new("Estimate: #{puzzle.xpath('estimate')[0].text} minutes, ", puzzle.xpath('estimate')[0].text, exclude_if: '0')} |
#{MaybeText.new("role: #{puzzle.xpath('role')[0].text}.", puzzle.xpath('role')[0].text, exclude_if: 'IMP')} |
\
If you have any technical questions, don't ask me, |
submit new tickets instead. The task will be \"done\" when |
the problem is fixed and the text of the puzzle is |
_removed_ from the source code. Here is more about |
[PDD](http://www.yegor256.com/2009/03/04/pdd.html) and |
[about me](http://www.yegor256.com/2017/04/05/pdd-in-action.html). |
================================================
FILE: objects/templates/gitlab_tickets_body.haml
================================================
-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
-# SPDX-License-Identifier: MIT
The puzzle `#{puzzle.xpath('id')[0].text}` |
from ##{puzzle.xpath('ticket')[0].text} has to be resolved: |
\
#{url}
\
The puzzle was created by #{puzzle.xpath('author')[0].text} on |
#{Time.parse(puzzle.xpath('time')[0].text).strftime('%d-%b-%y')}. |
\
#{MaybeText.new("Estimate: #{puzzle.xpath('estimate')[0].text} minutes, ", puzzle.xpath('estimate')[0].text, exclude_if: '0')} |
#{MaybeText.new("role: #{puzzle.xpath('role')[0].text}.", puzzle.xpath('role')[0].text, exclude_if: 'IMP')} |
\
If you have any technical questions, don't ask me, |
submit new tickets instead. The task will be \"done\" when |
the problem is fixed and the text of the puzzle is |
_removed_ from the source code. Here is more about |
[PDD](http://www.yegor256.com/2009/03/04/pdd.html) and |
[about me](http://www.yegor256.com/2017/04/05/pdd-in-action.html). |
================================================
FILE: objects/templates/jira_tickets_body.haml
================================================
-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
-# SPDX-License-Identifier: MIT
The puzzle `#{puzzle.xpath('id')[0].text}` |
from ##{puzzle.xpath('ticket')[0].text} has to be resolved: |
\
#{url}
\
The puzzle was created by #{puzzle.xpath('author')[0].text} on |
#{Time.parse(puzzle.xpath('time')[0].text).strftime('%d-%b-%y')}. |
\
#{MaybeText.new("Estimate: #{puzzle.xpath('estimate')[0].text} minutes, ", puzzle.xpath('estimate')[0].text, exclude_if: '0')} |
#{MaybeText.new("role: #{puzzle.xpath('role')[0].text}.", puzzle.xpath('role')[0].text, exclude_if: 'IMP')} |
\
If you have any technical questions, don't ask me, |
submit new tickets instead. The task will be \"done\" when |
the problem is fixed and the text of the puzzle is |
_removed_ from the source code. Here is more about |
[PDD](http://www.yegor256.com/2009/03/04/pdd.html) and |
[about me](http://www.yegor256.com/2017/04/05/pdd-in-action.html). |
================================================
FILE: objects/tickets/commit_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Tickets that post into commits.
#
class CommitTickets
def initialize(vcs, tickets)
@vcs = vcs
@commit = vcs.repo.head_commit_hash
@tickets = tickets
end
def notify(issue, message)
@tickets.notify(issue, message)
end
def submit(puzzle)
done = @tickets.submit(puzzle)
return done if suppressed_repo?
@vcs.create_commit_comment(
@commit,
"Puzzle `#{puzzle.xpath('id')[0].text}` discovered in \
[`#{puzzle.xpath('file')[0].text}`](#{@vcs.file_link(puzzle.xpath('file')[0].text)}) \
and submitted as ##{done[:number]}. Please, remember that the puzzle was not \
necessarily added in this particular commit. Maybe it was added earlier, but \
we discovered it only now."
)
done
end
def close(puzzle)
done = @tickets.close(puzzle)
if done && !opts.include?('on-lost-puzzle')
@vcs.create_commit_comment(
@commit,
"Puzzle `#{puzzle.xpath('id')[0].text}` disappeared from \
[`#{puzzle.xpath('file')[0].text}`](#{@vcs.file_link(puzzle.xpath('file')[0].text)}), \
that's why I closed ##{puzzle.xpath('issue')[0].text}. \
Please, remember that the puzzle was not necessarily removed in this \
particular commit. Maybe it happened earlier, but we discovered this fact \
only now."
)
end
done
end
private
def opts
array = @vcs.repo.config.dig('alerts', 'suppress')
array.nil? || !array.is_a?(Array) ? [] : array
end
def suppressed_repo?
suppressed_options = %w[on-found-puzzle on-scope]
suppressed_options.any? { |item| opts.include?(item) }
end
end
================================================
FILE: objects/tickets/emailed_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Tickets that email when submitted or closed.
#
class EmailedTickets
def initialize(vcs, tickets)
@vcs = vcs
@tickets = tickets
end
def notify(issue, message)
@tickets.notify(issue, message)
end
def submit(puzzle)
done = @tickets.submit(puzzle)
issue_link = @vcs.issue_link(done[:number])
file_link = @vcs.file_link(puzzle.xpath('file')[0].text)
Mail.new do
from '0pdd <no-reply@0pdd.com>'
to 'admin@0pdd.com'
subject "#{issue_link} opened"
text_part do
content_type 'text/plain; charset=UTF-8'
body "Hey,\n\n\
Issue #{done[:href]} opened.\n\n\
ID: #{puzzle.xpath('id')[0].text}\n\
File: #{puzzle.xpath('file')[0].text}\n\
Lines: #{puzzle.xpath('lines')[0].text}\n\
Here: #{file_link}\
##{puzzle.xpath('lines')[0].text.gsub(/(\d+)/, 'L\1')}\n\
Author: #{puzzle.xpath('author')[0].text}\n\
Time: #{puzzle.xpath('time')[0].text}\n\
Estimate: #{puzzle.xpath('estimate')[0].text} minutes\n\
Role: #{puzzle.xpath('role')[0].text}\n\n\
Body: #{puzzle.xpath('body')[0].text}\n\n\
Thanks,\n\
0pdd"
end
end.deliver!
done
end
def close(puzzle)
done = @tickets.close(puzzle)
if done
issue_number = puzzle.xpath('issue')[0].text
issue_link = @vcs.issue_link(issue_number)
Mail.new do
from '0pdd <no-reply@0pdd.com>'
to 'admin@0pdd.com'
subject "#{issue_link} closed"
text_part do
content_type 'text/plain; charset=UTF-8'
body "Hey,\n\n\
Issue #{issue_link} closed.\n\n\
Thanks,\n\
0pdd"
end
end.deliver!
end
done
end
end
================================================
FILE: objects/tickets/logged_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'cgi'
require_relative '../truncated'
require_relative '../user_error'
#
# Tickets that are logged.
#
class LoggedTickets
def initialize(vcs, log, tickets)
@vcs = vcs
@log = log
@tickets = tickets
end
def notify(issue, message)
@tickets.notify(issue, message)
end
def submit(puzzle)
tag = "#{puzzle.xpath('id')[0].text}/submit"
if @log.exists(tag)
raise UserError, "Tag \"#{tag}\" already exists, won't submit again. \
This situation most probably means that \
this puzzle was already seen in the code and \
you're trying to create it again. We would recommend you to re-phrase \
the text of the puzzle and push again. If this doesn't work, please let us know \
in GitHub: https://github.com/yegor256/0pdd/issues. More details here: \
https://www.0pdd.com/log-item?repo=#{CGI.escape(@vcs.repo.name)}&tag=#{CGI.escape(tag)}&vcs=#{@vcs.name.downcase} ."
end
done = @tickets.submit(puzzle)
@log.put(
tag,
"#{puzzle.xpath('id')[0].text} submitted in issue ##{done[:number]}: \
\"#{Truncated.new(puzzle.xpath('body')[0].text, 100)}\" \
at #{puzzle.xpath('file')[0].text}; #{puzzle.xpath('lines')[0].text}"
)
done
end
def close(puzzle)
done = @tickets.close(puzzle)
if done
tag = "#{puzzle.xpath('id')[0].text}/closed"
if @log.exists(tag)
raise UserError, "Tag \"#{tag}\" already exists, won't close again. \
This is a rare and rather unusual bug. Please report it to us: \
https://github.com/yegor256/0pdd/issues. More details here: \
https://www.0pdd.com/log-item?repo=#{CGI.escape(@vcs.repo.name)}&tag=#{CGI.escape(tag)}&vcs=#{@vcs.name.downcase} ."
end
@log.put(
tag,
"#{puzzle.xpath('id')[0].text} closed in issue \
##{puzzle.xpath('issue')[0].text}"
)
end
done
end
end
================================================
FILE: objects/tickets/milestone_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Tickets that inherit milestones.
#
class MilestoneTickets
def initialize(vcs, tickets)
@vcs = vcs
@tickets = tickets
end
def notify(issue, message)
@tickets.notify(issue, message)
end
def submit(puzzle)
submitted = @tickets.submit(puzzle)
config = @vcs.repo.config
if config['tickets']&.include?('inherit-milestone') &&
puzzle.xpath('ticket')[0].text =~ /[0-9]+/
num = puzzle.xpath('ticket')[0].text.to_i
parent = @vcs.issue(num)
unless parent.nil? || parent[:milestone].nil?
begin
@vcs.update_issue(
num,
milestone: parent[:milestone][:number]
)
unless config.dig('alerts', 'suppress')
&.include?('on-inherited-milestone')
@vcs.add_comment(
submitted[:number],
"This puzzle inherited milestone \
`#{parent[:milestone][:title]}` from issue ##{num}."
)
end
rescue Octokit::Error, Gitlab::Error::Error, JIRA::Error::Error => e
@vcs.add_comment(
submitted[:number],
"For some reason I wasn't able to set milestone \
`#{parent[:milestone][:title]}`, inherited from `#{num}`, \
to this issue. Please, \
[submit a ticket](https://github.com/yegor256/0pdd/issues/new) \
to us with the text you see below:\
\n\n```#{e.class.name}\n#{e.message}\n#{e.backtrace.join("\n")}\n```"
)
end
end
end
submitted
end
def close(puzzle)
@tickets.close(puzzle)
end
end
================================================
FILE: objects/tickets/sentry_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'mail'
require 'sentry-ruby'
require_relative '../user_error'
require_relative '../truncated'
#
# Tickets that report to Sentry.
#
class SentryTickets
def initialize(tickets)
@tickets = tickets
end
def notify(issue, message)
@tickets.notify(issue, message)
rescue UserError => e
puts e.message
rescue Exception => e
Sentry.capture_exception(e)
email(e)
raise e
end
def submit(puzzle)
@tickets.submit(puzzle)
rescue UserError => e
puts e.message
nil
rescue Exception => e
Sentry.capture_exception(e)
email(e)
raise e
end
def close(puzzle)
@tickets.close(puzzle)
rescue UserError => e
puts e.message
true
rescue Exception => e
Sentry.capture_exception(e)
email(e)
raise e
end
private
def email(e)
mail = Mail.new do
from '0pdd <no-reply@0pdd.com>'
to 'admin@0pdd.com'
subject Truncated.new(e.message).to_s
text_part do
content_type 'text/plain; charset=UTF-8'
body "Hi,\n\n\
#{e.message}\n\n
#{e.backtrace.join("\n")}\n\n
Thanks,\n\
0pdd"
end
html_part do
content_type 'text/html; charset=UTF-8'
body "<html><body><p>Hi,</p>
<pre>#{e.message}\n\n#{e.backtrace.join("\n")}</pre>
</body></html>"
end
end
mail.deliver!
end
end
================================================
FILE: objects/tickets/tagged_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Tagged tickets.
#
class TaggedTickets
def initialize(vcs, tickets)
@vcs = vcs
@tickets = tickets
end
def notify(issue, message)
@tickets.notify(issue, message)
end
def submit(puzzle)
issue = @tickets.submit(puzzle)
issue_id = issue[:number]
yaml = @vcs.repo.config
if yaml['tags'].is_a?(Array)
tags = yaml['tags'].map { |x| x.strip.downcase }
labels = @vcs.labels
.map { |json| json[:name] }
.map { |x| x.strip.downcase }
needed = tags - labels
begin
needed.each { |t| @vcs.add_label(t, 'F74219') }
@vcs.add_labels_to_an_issue(issue_id, tags)
rescue Octokit::Error, Gitlab::Error::Error, JIRA::Error::Error => e
@vcs.add_comment(
issue_id,
"I can't create #{@vcs.name} labels `#{needed.join('`, `')}`. \
Most likely I don't have necessary permissions to `#{@vcs.repo.name}` repository. \
Please, make sure @0pdd user is in the \
[list of collaborators](#{@vcs.collaborators_link}):\
\n\n```#{e.class.name}\n#{e.message}\n#{e.backtrace.join("\n")}\n```"
)
rescue Octokit::NotFound, Gitlab::Error::NotFound, JIRA::Error::NotFound => e
@vcs.add_comment(
issue_id,
"For some reason I wasn't able to add #{@vcs.name} labels \
`#{needed.join('`, `')}` to this issue \
(required=`#{tags.join('`, `')}`; existing=`#{labels.join('`, `')}`). \
Please, [submit a ticket](https://github.com/yegor256/0pdd/issues/new) \
to us with the text you see below:\
\n\n```#{e.class.name}\n#{e.message}\n#{e.backtrace.join("\n")}\n```"
)
end
end
issue
end
def close(puzzle)
@tickets.close(puzzle)
end
end
================================================
FILE: objects/tickets/tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'haml'
require_relative '../truncated'
require_relative '../maybe_text'
#
# One ticket.
#
class Tickets
def initialize(vcs)
@vcs = vcs
end
def notify(issue, message)
@vcs.add_comment(
issue,
"@#{@vcs.issue(issue)[:author][:username]} #{message}"
)
rescue Octokit::NotFound, Gitlab::NotFound, JIRA::NotFound => e
puts "The issue most probably is not found, can't comment: #{e.message}"
end
def submit(puzzle)
data = { title: title(puzzle), description: body(puzzle) }
issue = @vcs.create_issue(data)
unless users.empty?
@vcs.add_comment(
issue[:number],
(users + ['please pay attention to this new issue.']).join(' ')
)
end
{ number: issue[:number], href: issue[:html_url] }
end
def close(puzzle)
issue = puzzle.xpath('issue')[0].text
return true if @vcs.issue(issue)[:state] == 'closed'
@vcs.close_issue(issue)
@vcs.add_comment(
issue,
[
"The puzzle `#{puzzle.xpath('id')[0].text}` has disappeared",
" from the source code, that's why I closed this issue.",
(users.empty? ? '' : " //cc #{users.join(' ')}")
].join
)
true
end
private
def users
yaml = @vcs.repo.config
if !yaml.nil? && yaml['alerts'] && yaml['alerts'][@vcs.name.downcase]
yaml['alerts'][@vcs.name.downcase]
.map { |x| x.strip.downcase }
.map { |n| n.gsub(/[^0-9a-zA-Z-]+/, '') }
.map { |n| n[0..64] }
.map { |n| "@#{n}" }
else
[]
end
end
def title(puzzle)
yaml = @vcs.repo.config
format = []
format += yaml['format'].map { |x| x.strip.downcase } if !yaml.nil? && yaml['format'].is_a?(Array)
len = format.find { |i| i =~ /title-length=\d+/ }
Truncated.new(
if format.include?('short-title')
puzzle.xpath('body')[0].text
else
subject = File.basename(puzzle.xpath('file')[0].text)
start, stop = puzzle.xpath('lines')[0].text.split('-')
[
subject,
':',
(start == stop ? start : "#{start}-#{stop}"),
": #{puzzle.xpath('body')[0].text}"
].join
end,
[[len ? len.gsub(/^title-length=/, '').to_i : 60, 30].max, 255].min
).to_s
end
def body(puzzle)
file = puzzle.xpath('file')[0].text
start, stop = puzzle.xpath('lines')[0].text.split('-')
sha = @vcs.repo.head_commit_hash || vcs.repo.master
url = @vcs.puzzle_link_for_commit(sha, file, start, stop)
template = File.read(
File.join(File.dirname(__FILE__), "../templates/#{@vcs.name.downcase}_tickets_body.haml")
)
Haml::Engine.new(template).render(
Object.new, url: url, puzzle: puzzle
)
end
end
================================================
FILE: objects/truncated.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# Truncated text.
#
class Truncated
def initialize(text, max = 40, tail = '...')
@text = text
@max = max
@tail = tail
end
def to_s
clean = @text.gsub(/\s+/, ' ').strip
if @max < clean.length
limit = @max - @tail.length
stop = clean.rindex(' ', limit) || 0
"#{clean[0...stop]}#{@tail}"
else
clean
end
end
end
================================================
FILE: objects/user_error.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
#
# User Error
#
class UserError < StandardError
end
================================================
FILE: objects/vcs/github.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'octokit'
require_relative '../git_repo'
#
# Github VCS
#
class GithubRepo
attr_reader :repo, :name
def initialize(client, json, config = {})
@name = 'github'
@client = client
@config = config
@json = json
@repo = git_repo(json, config)
end
# Check whether this repository exists in GitHub and we have
# access to it. Well, the actual access permissions are not checked
# here, but we only try to read properties of the repo. If such a HTTP
# request fails, the method returns FALSE.
def exists?
@client.repository(@repo.name)
true
rescue Octokit::NotFound => e
puts "Repository #{@repo.name} is not available: #{e.message}"
false
end
# Read information about one issue in GitHub and return it
# as a map.
def issue(issue_id)
hash = @client.issue(@repo.name, issue_id)
id = hash[:user][:id] if hash[:user]
username = hash[:user][:login] if hash[:user]
{
state: hash[:state],
author: {
id: id,
username: username
},
milestone: hash[:milestone]
}
end
# @todo #312:30min Currently, if 0pdd fails to close an issue it causes all other downstream execution to be skipped
# therefore leaving the job in a non deterministic state. Catch and track the error here to
# prevent this from happening. Also applies to `add_comment(...)`
def close_issue(issue_id)
@client.close_issue(@repo.name, issue_id)
end
def create_issue(data)
fields = %i[title description]
options = data.reject { |k| fields.include? k }
@client.create_issue(
@repo.name,
data[:title],
data[:description],
options
)
end
def update_issue(issue_id, data)
@client.update_issue(@repo.name, issue_id, data)
end
def labels
@client.labels(@repo.name)
end
def add_label(label, color)
@client.add_label(@repo.name, label, color)
end
def add_labels_to_an_issue(issue_id, labels)
@client.add_labels_to_an_issue(@repo.name, issue_id, labels)
end
def add_comment(issue_id, comment)
@client.add_comment(@repo.name, issue_id, comment)
end
def create_commit_comment(sha, comment)
@client.create_commit_comment(@repo.name, sha, comment)
end
def list_commits
@client.commits(@repo.name)
end
def user(username)
@client.user(username)
end
def star
@client.star(@repo.name)
end
def repository_link
"https://github.com/#{@repo.name}"
end
def collaborators_link
"https://github.com/#{@repo.name}/settings/collaboration"
end
def file_link(file)
"https://github.com/#{@repo.name}/blob/#{@repo.master}/#{file})"
end
def puzzle_link_for_commit(sha, file, start, stop)
"https://github.com/#{@repo.name}/blob/#{sha}/#{file}#L#{start}-L#{stop}"
end
def issue_link(issue_id)
"https://github.com/#{@repo.name}/issues/#{issue_id}"
end
private
def git_repo(json, config)
uri = json['repository']['ssh_url'] || json['repository']['url']
target = json['ref']
name = json['repository']['full_name']
default_branch = json['repository']['master_branch']
head_commit_hash = json['head_commit'] ? json['head_commit']['id'] : ''
GitRepo.new(
uri: uri,
name: name,
id_rsa: config['id_rsa'],
target: target,
master: default_branch,
head_commit_hash: head_commit_hash
)
end
end
================================================
FILE: objects/vcs/gitlab.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'gitlab'
require_relative '../git_repo'
require_relative '../clients/gitlab'
#
# Gitlab repo
# API: https://github.com/NARKOZ/gitlab
#
class GitlabRepo
attr_reader :repo, :name
def initialize(client, json, config = {})
@name = 'github'
@client = client
@config = config
@json = json
@repo = git_repo(json, config)
end
def issue(issue_id)
hash = JSON.parse(
@client.issue(@repo.name, issue_id).to_hash.to_json,
symbolize_names: true
)
number, title = hash[:milestone].values_at(:id, :title) if hash[:milestone]
{
state: hash[:state],
author: hash[:author],
milestone: {
number: number,
title: title
}
}
rescue Gitlab::Error::NotFound => e
raise "The issue most probably is not found, can' comment: #{e.message}"
end
def close_issue(issue_id)
@client.close_issue(@repo.name, issue_id)
rescue Gitlab::Error::NotFound => e
raise "The issue most probably is not found, can't close: #{e.message}"
end
def create_issue(data)
options = data.reject { |k| k == :title }
hash = JSON.parse(
@client.create_issue(@repo.name, data[:title], options).to_hash.to_json,
symbolize_names: true
)
{ number: hash[:iid], html_url: hash[:web_url] }
end
def update_issue(issue_id, data)
@client.edit_issue(@repo.name, issue_id, data)
end
def labels
result = []
@client.labels(@repo.name).each_page do |page|
page.each do |label|
result << JSON.parse(
label.to_hash.to_json,
symbolize_names: true
)
end
end
result
end
def add_label(label, color)
@client.add_label(@repo.name, label, color)
end
def add_labels_to_an_issue(issue_id, labels)
options = { labels: labels }
@client.edit_issue(@repo.name, issue_id, options)
end
def add_comment(issue_id, comment)
@client.create_issue_note(@repo.name, issue_id, comment)
rescue Gitlab::Error::NotFound => e
raise "The issue most probably is not found, can't comment: #{e.message}"
end
def create_commit_comment(sha, comment)
hash = JSON.parse(
@client.create_commit_comment(@repo.name, sha, comment).to_hash.to_json,
symbolize_names: true
)
hash[:html_url] = "https://gitlab.com/#{@repo.name}/commit/#{sha}"
hash
end
def list_commits
commits = []
@client.commits(@repo.name).each_page do |page|
page.each do |commit|
commits << { sha: commit.id, html_url: commit.web_url }
end
end
commits
end
def user(username)
hash = JSON.parse(
@client.user(username).to_hash.to_json,
symbolize_names: true
)
hash[:email] = hash[:public_email]
hash
end
def star
@client.star_project(@repo.name)
end
def exists?
hash = JSON.parse(
@client.project(@repo.name).to_hash.to_json,
symbolize_names: true
)
hash[:private] = hash[:visibility] == 'private'
true
rescue Gitlab::Error::NotFound => e
puts "Repository #{@repo.name} is not available: #{e.message}"
false
rescue Gitlab::Error::Forbidden => e
puts "Repository #{@repo.name} is not accessible: #{e.message}"
false
end
def repository_link
"https://gitlab.com/#{@repo.name}"
end
def collaborators_link
"https://gitlab.com/#{@repo.name}/project_members"
end
def file_link(file)
"https://gitlab.com/#{@repo.name}/blob/#{@repo.master}/#{file})"
end
def puzzle_link_for_commit(sha, file, start, stop)
"https://gitlab.com/#{@repo.name}/blob/#{sha}/#{file}#L#{start}-L#{stop}"
end
def issue_link(issue_id)
"https://gitlab.com/#{@repo.name}/issues/#{issue_id}"
end
private
def git_repo(json, config)
uri = json['project']['url']
name = json['project']['path_with_namespace']
target = json['ref']
default_branch = json['project']['default_branch']
head_commit_hash = json['checkout_sha']
GitRepo.new(
uri: uri,
name: name,
target: target,
id_rsa: config['id_rsa'],
master: default_branch,
head_commit_hash: head_commit_hash
)
end
end
================================================
FILE: objects/vcs/jira.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'jira-ruby'
require_relative '../git_repo'
#
# Jira VCS
#
class JiraRepo
attr_reader :repo, :name
def initialize(client, json, config = {})
@name = 'JIRA'
@client = client
@config = config
@json = json
@repo = git_repo(json, config)
end
def issue(issue_id)
@client.Issue.find(issue_id)
end
def close_issue(issue_id)
issue = @client.Issue.find(issue_id)
issue.save(
'fields' => {
'summary' => data[:description],
'project' => { 'id' => data[:repo] },
'issuetype' => { 'id' => '3' },
'status' => 'closed'
}
)
issue.fetch
end
def create_issue(data)
issue = @client.Issue.build
issue.save(
'fields' => {
'summary' => data[:description],
'project' => { 'id' => data[:repo] },
'issuetype' => { 'id' => '3' }
}
)
issue.fetch
end
def update_issue(issue_id, data)
issue = @client.Issue.find(issue_id)
issue.save(
'fields' => {
'summary' => data[:description],
'project' => { 'id' => data[:repo] },
'issuetype' => { 'id' => '3' }
}
)
issue.fetch
end
def exists?
@client.Project.find(@repo.name)
true
rescue JIRA::NotFound => e
puts "Repository #{@repo.name} is not available: #{e.message}"
false
end
def repository_link
"https://your-domain.atlassian.net/rest/api/3/project#{@repo.name}"
end
private
def git_repo(json, config)
uri = json['repository']['ssh_url'] || json['repository']['url']
name = json['repository']['full_name']
default_branch = json['repository']['master_branch']
head_commit_hash = json['head_commit']['id']
GitRepo.new(
uri: uri,
name: name,
id_rsa: config['id_rsa'],
master: default_branch,
head_commit_hash: head_commit_hash
)
end
end
================================================
FILE: renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}
================================================
FILE: test/fake_github.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
class FakeGithub
attr_reader :name, :repo
def initialize(options = {})
@name = 'GITHUB'
@memberships = options[:memberships] || [
{
'state' => 'pending',
'organization' => {
'login' => 'github'
}
}, {
'state' => 'pending',
'organization' => {
'login' => 'zerocracy'
}
}
]
@invitations = options[:invitations] || [
{
'id' => 1001,
'repository' => {
'name' => 'yegor256/0pdd'
}
}, {
'id' => 1023,
'repository' => {
'name' => 'yegor256/sixnines'
}
}
]
@repositories = options[:repositories] || []
@repo = options[:repo]
end
def rate_limit
limit = Object.new
def limit.remaining
4096
end
limit
end
def update_organization_membership(org, options = {})
return unless options['state']
@memberships.find do |m|
m['organization']['login'] == org
end['state'] = options['state']
end
def organization_memberships(options = {})
if options['state']
@memberships.find_all { |m| m['state'] == options['state'] }
else
@memberships
end
end
def user_repository_invitations(_options = {})
@invitations
end
def accept_repository_invitation(id, _options = {})
invitation = @invitations.find { |i| i['id'] == id }
return false if invitation.nil?
@repositories.push(invitation['repository']['name'])
true
end
def repositories(user = nil, _options = {})
@repositories unless user
end
def issue(_)
{
state: 'open',
author: {
id: '1',
username: 'yegor256'
},
milestone: {
number: 1,
title: 'v0.1'
}
}
end
def close_issue(_); end
def create_issue(_)
{
number: 1,
html_url: 'url'
}
end
def update_issue(_, _); end
def labels
[
{
id: ``,
name: 'Dev',
color: '#ff00ff'
}
]
end
def add_label(_, _); end
def add_labels_to_an_issue(_, _); end
def add_comment(_, _); end
def create_commit_comment(_, _, _)
{
html_url: 'url'
}
end
def list_commits
[
{
sha: '123456',
html_url: 'url'
}
]
end
def user(_)
{
name: 'foobar',
email: 'foobar@example.com'
}
end
def star; end
def repository(_ = nil)
{
private: false
}
end
def repository_link
"https://github.com/#{@repo.name}"
end
def collaborators_link
"https://github.com/#{@repo.name}/settings/collaboration"
end
def file_link(file)
"https://github.com/#{@repo.name}/blob/#{@repo.master}/#{file})"
end
def puzzle_link_for_commit(sha, file, start, stop)
"https://github.com/#{@repo.name}/blob/#{sha}/#{file}#L#{start}-L#{stop}"
end
def issue_link(issue_id)
"https://github.com/#{@repo.name}/issues/#{issue_id}"
end
private
def git_repo
# Output:
# repo -> GitRepo
raise NotImplementedError, 'You must implement this method'
end
end
================================================
FILE: test/fake_gitlab.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
class FakeGitlab
attr_reader :name, :repo
def initialize(options = {})
@name = 'GITLAB'
@repositories = options[:repositories] || []
@projects = options[:projects] || []
@repo = options[:repo]
end
def repositories(user = nil, _options = {})
@repositories unless user
end
def issue(_)
{
state: 'open',
author: {
id: '1',
username: 'yegor256'
},
milestone: {
number: 1,
title: 'v0.1'
}
}
end
def close_issue(_); end
def create_issue(_)
{
number: 1,
html_url: 'url'
}
end
def update_issue(_, _); end
def labels
[
{
id: ``,
name: 'Dev',
color: '#ff00ff'
}
]
end
def add_label(_, _); end
def add_labels_to_an_issue(_, _); end
def add_comment(_, _); end
def create_commit_comment(_, _)
{
html_url: 'url'
}
end
def list_commits
[
{
sha: '123456',
html_url: 'url'
}
]
end
def user(_)
{
name: 'foobar',
email: 'foobar@example.com'
}
end
def star; end
def repository(_ = nil)
{
private: false
}
end
def project(_ = nil)
{
private: false
}
end
def repository_link
"https://gitlab.com/#{@repo.name}"
end
def collaborators_link
"https://gitlab.com/#{@repo.name}/project_members"
end
def file_link(file)
"https://gitlab.com/#{@repo.name}/blob/#{@repo.master}/#{file})"
end
def puzzle_link_for_commit(sha, file, start, stop)
"https://gitlab.com/#{@repo.name}/blob/#{sha}/#{file}#L#{start}-L#{stop}"
end
def issue_link(issue_id)
"https://gitlab.com/#{@repo.name}/issues/#{issue_id}"
end
private
def git_repo
# Output:
# repo -> GitRepo
raise NotImplementedError, 'You must implement this method'
end
end
================================================
FILE: test/fake_log.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
class FakeLog
attr_reader :tag, :title
def exists(_)
false
end
def put(tag, text)
@title = text
@tag = tag
end
def get(_tag); end
def delete(_time, _tag); end
def list(_since = Time.now.to_i)
[]
end
end
================================================
FILE: test/fake_repo.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
require 'tempfile'
class FakeRepo
attr_reader :name, :config
def initialize(options = {})
@name = options[:name] || 'GITHUB'
@config = options[:config] || {}
end
def lock
Tempfile.new('0pdd-lock')
end
def xml
Nokogiri::XML('<puzzles date="2016-12-10T16:26:36Z"/>')
end
def push
# nothing here
end
end
================================================
FILE: test/fake_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
require 'tempfile'
class FakeStorage
def initialize(
dir = Dir.mktmpdir,
xml = '<puzzles date="2016-12-10T16:26:36Z" version="0.1"/>'
)
@file = File.join(dir, 'storage.xml')
save(xml)
end
def load
Nokogiri.XML(File.read(@file))
end
def save(xml)
File.write(@file, xml.to_s)
end
end
================================================
FILE: test/fake_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
class FakeTickets
attr_reader :submitted, :closed
def initialize
@submitted = []
@closed = []
end
def submit(puzzle)
@submitted << puzzle.xpath('id')[0].text
{ number: '123', href: 'http://0pdd.com' }
end
def close(puzzle)
@closed << puzzle.xpath('id')[0].text
true
end
end
================================================
FILE: test/test_0pdd.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'rack/test'
require_relative 'test__helper'
require_relative '../0pdd'
class AppTest < Minitest::Test
include Rack::Test::Methods
def app
Sinatra::Application
end
def test_renders_version
get('/version')
assert_predicate(last_response, :ok?)
end
def test_robots_txt
get('/robots.txt')
assert_predicate(last_response, :ok?)
end
def test_it_renders_home_page
get('/')
assert_predicate(last_response, :ok?)
assert_includes(last_response.body, '0pdd')
end
def test_renders_some_pages
[
'/',
'/robots.txt',
'/version',
'/puzzles.xsd',
'/logout',
'/css/main.css'
].each do |page|
get(page)
assert_operator(last_response.status, :<, 400, "Failed to render #{page}")
end
end
def test_it_renders_puzzles_xsd
get('/puzzles.xsd')
assert_predicate(last_response, :ok?)
assert_includes(last_response.body, '<xs:schema')
end
def test_renders_log_page
repo = 'yegor256/0pdd'
log = Log.new(Dynamo.new.aws, repo)
log.put('some-tag', 'some text here')
get("/log?name=#{repo}")
assert_predicate(last_response, :ok?, last_response.body)
assert_includes(last_response.body, repo, last_response.body)
assert_includes(last_response.body, 'some text', last_response.body)
end
def test_renders_log_item
repo = 'yegor256/0pdd'
log = Log.new(Dynamo.new.aws, repo)
tag = 'some-tag'
log.put(tag, 'some text here')
get("/log-item?repo=#{repo}&tag=#{tag}")
assert_predicate(last_response, :ok?, last_response.body)
assert_includes(last_response.body, repo, last_response.body)
assert_includes(last_response.body, 'some text', last_response.body)
end
def test_renders_page_not_found
get('/the-url-that-is-absent')
assert_equal(404, last_response.status)
end
def test_it_understands_push_from_github
headers = {
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => 'GitHub-Hookshot',
'HTTP_X_GITHUB_EVENT' => 'push'
}
post(
'/hook/github',
['{"head_commit":{"id":"-"},',
'"repository":{"url":"localhost",',
'"full_name":"yegor256-one/com.github.0pdd-test"},',
'"ref":"refs/heads/master"}'].join,
headers
)
assert_predicate(last_response, :ok?)
assert_includes(last_response.body, 'Thanks')
end
def test_it_ignores_push_from_github_to_not_master
headers = {
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => 'GitHub-Hookshot',
'HTTP_X_GITHUB_EVENT' => 'push'
}
post(
'/hook/github',
['{"head_commit":{"id":"-"},',
'"repository":{"url":"localhost",',
'"full_name":"yegor256-one/com.github.0pdd-test"},',
'"ref":"refs/heads/main"}'].join,
headers
)
assert_predicate(last_response, :ok?)
assert_includes(last_response.body, 'Thanks')
assert_includes(last_response.body, 'nothing is done')
end
def test_it_accepts_push_from_github_to_not_default_master
headers = {
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => 'GitHub-Hookshot',
'HTTP_X_GITHUB_EVENT' => 'push'
}
post(
'/hook/github',
['{"head_commit":{"id":"-"},',
'"repository":{"url":"localhost",',
'"master_branch": "main",',
'"full_name":"yegor256-one/com.github.0pdd-test"},',
'"ref":"refs/heads/main"}'].join,
headers
)
assert_predicate(last_response, :ok?)
assert_includes(last_response.body, 'Thanks')
refute_includes(last_response.body, 'nothing is done')
end
def test_it_ignore_push_from_github_to_not_default_master
headers = {
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => 'GitHub-Hookshot',
'HTTP_X_GITHUB_EVENT' => 'push'
}
post(
'/hook/github',
['{"head_commit":{"id":"-"},',
'"repository":{"url":"localhost",',
'"master_branch": "main",',
'"full_name":"yegor256-one/com.github.0pdd-test"},',
'"ref":"refs/heads/master"}'].join,
headers
)
assert_predicate(last_response, :ok?)
assert_includes(last_response.body, 'Thanks')
assert_includes(last_response.body, 'nothing is done')
end
def test_it_understands_push_from_gitlab
headers = {
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => 'GitLab 16.6',
'HTTP_X_GITLAB_EVENT' => 'Push Hook'
}
post(
'/hook/gitlab',
['{"checkout_sha": "da1560886d4",',
'"project":{"url":"localhost",',
'"path_with_namespace":"yegor256-one/com.github.0pdd-test"},',
'"ref":"refs/heads/master"}'].join,
headers
)
assert_predicate(last_response, :ok?)
assert_includes(last_response.body, 'Thanks')
end
def test_it_ignores_push_from_gitlab_to_not_master
headers = {
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => 'GitLab 16.6',
'HTTP_X_GITLAB_EVENT' => 'Push Hook'
}
post(
'/hook/gitlab',
['{"checkout_sha": "da1560886d4",',
'"project":{"url":"localhost",',
'"path_with_namespace":"yegor256-one/com.github.0pdd-test"},',
'"ref":"refs/heads/main"}'].join,
headers
)
assert_predicate(last_response, :ok?)
assert_includes(last_response.body, 'Thanks')
assert_includes(last_response.body, 'nothing is done')
end
def test_it_accepts_push_from_gitlab_to_not_default_master
headers = {
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => 'GitLab 16.6',
'HTTP_X_GITLAB_EVENT' => 'Push Hook'
}
post(
'/hook/gitlab',
['{"checkout_sha": "da1560886d4",',
'"project":{"url":"localhost",',
'"default_branch": "main",',
'"path_with_namespace":"yegor256-one/com.github.0pdd-test"},',
'"ref":"refs/heads/main"}'].join,
headers
)
assert_predicate(last_response, :ok?)
assert(last_response.body.start_with?('Thanks'))
refute_includes(last_response.body, 'nothing is done')
end
def test_it_ignores_push_from_gitlab_to_not_default_master
headers = {
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => 'GitLab 16.6',
'HTTP_X_GITLAB_EVENT' => 'Push Hook'
}
post(
'/hook/gitlab',
['{"checkout_sha": "da1560886d4",',
'"project":{"url":"localhost",',
'"default_branch": "main",',
'"path_with_namespace":"yegor256-one/com.github.0pdd-test"},',
'"ref":"refs/heads/master"}'].join,
headers
)
assert_predicate(last_response, :ok?)
assert_includes(last_response.body, 'Thanks')
assert_includes(last_response.body, 'nothing is done')
end
def test_renders_html_puzzles
get('/p?name=yegor256/pdd')
assert_predicate(last_response, :ok?)
html = last_response.body
assert(
html.include?('<html') &&
html.include?('<title>'),
"broken HTML: #{html}"
)
end
def test_snapshots_unavailable_repo
get('/snapshot?name=yegor256/0pdd_foobar_unavailable')
assert_equal(400, last_response.status)
end
def test_renders_svg_puzzles
get('/svg?name=yegor256/pdd')
assert_predicate(last_response, :ok?)
svg = last_response.body
File.write('/tmp/0pdd-button.svg', svg)
assert_includes(
svg, '<svg ',
"broken SVG: #{svg}"
)
end
def test_renders_xml_puzzles
get('/xml?name=yegor256/pdd')
assert_predicate(last_response, :ok?)
xml = last_response.body
assert_includes(
xml, '<puzzles ',
"broken XML: #{xml}"
)
end
def test_rejects_invalid_repo_name
get('/svg?name=yego256/pdd+a')
refute_predicate(last_response, :ok?)
end
def test_not_found
get('/unknown_path')
assert_equal(404, last_response.status)
assert_equal('text/html;charset=utf-8', last_response.content_type)
end
end
================================================
FILE: test/test__helper.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
ENV['RACK_ENV'] = 'test'
require 'simplecov'
require 'simplecov-cobertura'
unless SimpleCov.running || ENV['PICKS']
SimpleCov.command_name('test')
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
[
SimpleCov::Formatter::HTMLFormatter,
SimpleCov::Formatter::CoberturaFormatter
]
)
SimpleCov.minimum_coverage 65
SimpleCov.minimum_coverage_by_file 10
SimpleCov.start do
add_filter 'test/'
add_filter 'vendor/'
add_filter 'target/'
track_files 'lib/**/*.rb'
track_files '*.rb'
end
end
require 'minitest/autorun'
require 'minitest/reporters'
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
Minitest.load :minitest_reporter
require 'ostruct'
def object(hash)
json = hash.to_json
JSON.parse(json, object_class: OpenStruct)
end
================================================
FILE: test/test_cached_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'test__helper'
require_relative 'fake_storage'
require_relative '../objects/storage/cached_storage'
# CachedStorage test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestCachedStorage < Minitest::Test
def test_simple_xml_loading
Dir.mktmpdir do |dir|
storage = CachedStorage.new(FakeStorage.new, File.join(dir, 'a/b/z.xml'))
storage.save(Nokogiri::XML('<test>hello</test>'))
assert_equal('hello', storage.load.xpath('/test/text()')[0].text)
end
end
end
================================================
FILE: test/test_commit_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'yaml'
require_relative 'test__helper'
require_relative '../objects/tickets/commit_tickets'
# CommitTickets test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestCommitTickets < Minitest::Test
def test_submits_tickets
config = YAML.safe_load(
"
alerts:
suppress:
- on-found-puzzle"
)
vcs = object(repo: { config: config })
tickets = Object.new
def tickets.submit(_)
{}
end
tickets = CommitTickets.new(vcs, tickets)
tickets.submit(nil)
end
def test_closes_tickets
config = YAML.safe_load(
"
alerts:
suppress:
- on-lost-puzzle"
)
vcs = object(repo: { config: config })
tickets = Object.new
def tickets.close(_)
{}
end
tickets = CommitTickets.new(vcs, tickets)
tickets.close(nil)
end
def test_scope_suppressed_repo_should_be_quiet
config = YAML.safe_load(
"
alerts:
suppress:
- on-found-puzzle"
)
vcs = object(repo: { config: config })
tickets = Object.new
def tickets.submit(_)
{}
end
tickets = CommitTickets.new(vcs, tickets)
tickets.submit(nil)
end
end
================================================
FILE: test/test_credentials.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'mail'
require 'yaml'
require 'octokit'
require 'tmpdir'
require 'aws-sdk-dynamodb'
require_relative 'test__helper'
require_relative '../objects/storage/s3'
require_relative '../objects/tickets/tickets'
require_relative '../objects/log'
require_relative '../objects/vcs/github'
require_relative '../objects/git_repo'
class CredentialsTest < Minitest::Test
def test_connects_to_git_via_ssh
cfg = config
Dir.mktmpdir 'test' do |d|
repo = GitRepo.new(
uri: 'git@github.com:yegor256/0pdd',
name: 'yegor256/0pdd',
id_rsa: cfg['id_rsa'],
dir: d
)
repo.push
refute_nil(repo.xml.xpath('//puzzles'))
end
end
def test_connects_to_aws_dynamo
cfg = config
dynamo = Aws::DynamoDB::Client.new(
region: cfg['dynamo']['region'],
access_key_id: cfg['dynamo']['key'],
secret_access_key: cfg['dynamo']['secret']
)
refute(Log.new(dynamo, 'yegor256/0pdd').exists('some stupid tag'))
end
def test_connects_to_github
cfg = config
github = Octokit::Client.new(
access_token: cfg['github']['token']
)
tickets = Tickets.new(
GithubRepo.new(
github,
{
'repository' => {
'full_name' => 'yegor256/0pdd',
'url' => 'https://github.com/yegor256/0pdd',
'master_branch' => 'master'
},
'ref' => 'master',
'head_commit' => {
'id' => '---'
}
}
)
)
tickets.close(
Nokogiri::XML(
'<puzzle><id>AA</id><issue>1</issue></puzzle>'
).xpath('/puzzle')
)
end
def test_connects_to_aws_s3
cfg = config
storage = S3.new(
'yegor256/0pdd.xml',
cfg['s3']['bucket'],
cfg['s3']['region'],
cfg['s3']['key'],
cfg['s3']['secret']
)
refute_nil(storage.load.xpath('//puzzles'))
end
def test_sends_email_via_smtp
cfg = config
Mail.defaults do
delivery_method(
:smtp,
address: cfg['smtp']['host'],
port: cfg['smtp']['port'],
user_name: cfg['smtp']['user'],
password: cfg['smtp']['password'],
domain: '0pdd.com',
enable_starttls_auto: true
)
end
mail = Mail.new do
from '0pdd <no-reply@0pdd.com>'
to 'admin@0pdd.com'
subject 'Test email, ignore it'
text_part do
content_type 'text/plain; charset=UTF-8'
body 'It it a test email, ignore it.'
end
end
mail.deliver!
end
private
def config
file = File.join(File.dirname(__FILE__), '../config.yml')
file = ENV['PDD_CONFIG'] if ENV['PDD_CONFIG']
skip('...') unless File.exist?(file)
YAML.safe_load(File.open(file))
end
end
================================================
FILE: test/test_diff.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
require 'ostruct'
require_relative 'test__helper'
require_relative '../objects/diff'
# Diff test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestDiff < Minitest::Test
def test_notification_on_one_new_puzzle
tickets = Tickets.new
Diff.new(
Nokogiri::XML('<puzzles/>'),
Nokogiri::XML(
'<puzzles>
<puzzle alive="true">
<id>1-abcdef</id>
<issue>5</issue>
<children>
<puzzle alive="true">
<id>5-abcdef</id>
<issue href="#">6</issue>
<ticket>5</ticket>
<children>
</children>
</puzzle>
</children>
</puzzle>
</puzzles>'
)
).notify(tickets)
assert_equal(
1, tickets.messages.length,
"Incorrect number of messages: #{tickets.messages.length}"
)
assert_equal(
'5 the puzzle [#6](#) is still not solved.', tickets.messages[0],
"Text is wrong: #{tickets.messages[0]}"
)
end
def test_notification_unknown_issue
tickets = Tickets.new
xml = File.open('test-assets/puzzles/notify-unknown-open-issues.xml') do |f|
Nokogiri::XML(f)
end
Diff.new(Nokogiri::XML('<puzzles/>'), xml).notify(tickets)
assert_equal(
1, tickets.messages.length,
"Incorrect number of messages: #{tickets.messages.length}"
)
assert_equal(
'5 the puzzle [#125](//issue/125) is still not solved.', tickets.messages[0],
"Text is wrong: #{tickets.messages[0]}"
)
end
def test_notification_on_two_new_puzzles
tickets = Tickets.new
Diff.new(
Nokogiri::XML('<puzzles/>'),
Nokogiri::XML(
'<puzzles>
<puzzle alive="true">
<id>1-abcdef</id>
<issue>55</issue>
<children>
<puzzle alive="true">
<id>5-abcdee</id>
<issue href="#">66</issue>
<ticket>55</ticket>
<children>
</children>
</puzzle>
<puzzle alive="true">
<id>5-abcded</id>
<issue href="#">77</issue>
<ticket>55</ticket>
<children>
</children>
</puzzle>
</children>
</puzzle>
</puzzles>'
)
).notify(tickets)
assert_equal(
1, tickets.messages.length,
"Incorrect number of messages: #{tickets.messages.length}"
)
assert_equal(
'55 2 puzzles [#66](#), [#77](#) are still not solved.', tickets.messages[0],
"Text is wrong: #{tickets.messages[0]}"
)
end
def test_notification_on_solved_puzzle
tickets = Tickets.new
before = Nokogiri::XML(
'<puzzles>
<puzzle alive="true">
<id>100-ffffff</id>
<issue>100</issue>
<ticket>500</ticket>
</puzzle>
</puzzles>'
)
after = Nokogiri::XML(before.to_s)
after.xpath('//puzzle[id="100-ffffff"]')[0]['alive'] = 'false'
Diff.new(before, after).notify(tickets)
assert_equal(
1, tickets.messages.length,
"Incorrect number of messages: #{tickets.messages.length}"
)
assert_equal(
'500 the only puzzle [#100]() is solved here.', tickets.messages[0],
"Text is wrong: #{tickets.messages[0]}"
)
end
def test_notification_on_one_solved_puzzle
tickets = Tickets.new
before = Nokogiri::XML(
'<puzzles>
<puzzle alive="true">
<id>100-1</id>
<issue>100</issue>
<ticket>999</ticket>
</puzzle>
<puzzle alive="false">
<id>100-2</id>
<issue>101</issue>
<ticket>999</ticket>
<children>
<puzzle alive="true">
<id>101-1</id>
<issue>13</issue>
<ticket>101</ticket>
</puzzle>
</children>
</puzzle>
</puzzles>'
)
after = Nokogiri::XML(before.to_s)
after.xpath('//puzzle[id="100-1"]')[0]['alive'] = 'false'
Diff.new(before, after).notify(tickets)
assert_equal(
1, tickets.messages.length,
"Wrong about of msgs (#{tickets.messages.length}): #{tickets.messages}"
)
assert_equal(
'999 the puzzle [#13]() is still not solved; solved: [#100](), [#101]().', tickets.messages[0],
"Text is wrong: #{tickets.messages[0]}"
)
end
def test_notification_on_update
tickets = Tickets.new
before = Nokogiri::XML(
'<puzzles>
<puzzle alive="true">
<id>1-abcdef</id>
<issue>5</issue>
<children>
<puzzle alive="true">
<id>5-abcdef</id>
<issue href="#">6</issue>
<ticket>5</ticket>
</puzzle>
</children>
</puzzle>
</puzzles>'
)
after = Nokogiri::XML(before.to_s)
after.xpath('//puzzle[id="5-abcdef"]')[0]['alive'] = 'false'
Diff.new(before, after).notify(tickets)
assert_equal(
1, tickets.messages.length,
"Wrong about of msgs (#{tickets.messages.length}): #{tickets.messages}"
)
assert_equal(
'5 the only puzzle [#6](#) is solved here.', tickets.messages[0],
"Text is wrong: #{tickets.messages[0]}"
)
end
def test_quiet_when_no_changes
tickets = Tickets.new
xml = '<puzzles>
<puzzle alive="true">
<id>1-abcdef</id>
<issue>50</issue>
<children>
<puzzle alive="true">
<id>50-abcdef</id>
<issue href="#">60</issue>
<children>
</children>
</puzzle>
</children>
</puzzle>
</puzzles>'
Diff.new(
Nokogiri::XML(xml),
Nokogiri::XML(xml)
).notify(tickets)
assert_empty(tickets.messages)
end
class Tickets
attr_reader :messages
def initialize
@messages = []
end
def notify(ticket, text)
@messages << "#{ticket} #{text}"
end
end
end
================================================
FILE: test/test_diff_complicated.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
require 'ostruct'
require_relative 'test__helper'
require_relative '../objects/diff'
# Complicated diff test.
class TestDiff < Minitest::Test
# @todo #234:15m Add tests for more complicated dynamics, like
# [here](https://github.com/php-coder/mystamps/issues/695#issuecomment-405372820).
# Ideally, this tests other cases that can lead to the observed behaviour,
# but not covered by the test suite.
def test_notification_on_parent_solved_with_others_unsolved
tickets = Tickets.new
before = Nokogiri::XML(
'<puzzles>
<puzzle alive="true">
<id>100-1</id>
<issue>100</issue>
<ticket>999</ticket>
</puzzle>
<puzzle alive="true">
<id>100-2</id>
<issue>101</issue>
<ticket>999</ticket>
<children>
<puzzle alive="true">
<id>101-1</id>
<issue>13</issue>
<ticket>101</ticket>
</puzzle>
</children>
</puzzle>
</puzzles>'
)
after = Nokogiri::XML(before.to_s)
after.xpath('//puzzle[id="100-2"]')[0]['alive'] = 'false'
Diff.new(before, after).notify(tickets)
assert_equal(
1, tickets.messages.length,
"Wrong about of msgs (#{tickets.messages.length}): #{tickets.messages}"
)
assert_equal(
'999 2 puzzles [#100](), [#13]() are still not solved; solved: [#101]().', tickets.messages[0],
"Text is wrong: #{tickets.messages[0]}"
)
end
class Tickets
attr_reader :messages
def initialize
@messages = []
end
def notify(ticket, text)
@messages << "#{ticket} #{text}"
end
end
end
================================================
FILE: test/test_git_repo.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'tmpdir'
require_relative 'test__helper'
require_relative '../objects/git_repo'
require_relative '../objects/user_error'
# GitRepo test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestGitRepo < Minitest::Test
def test_clone_and_pull
Dir.mktmpdir 'test' do |d|
_, uri = git(d)
repo = GitRepo.new(name: 'yegor256/pdd', dir: d, uri: uri)
repo.push
repo.push
assert_path_exists(File.join(repo.path, '.git'))
end
end
def test_merge_unrelated_histories
Dir.mktmpdir 'test' do |d|
path, uri = git(d, 'repo')
repo = GitRepo.new(name: 'yegor256/pdd', dir: d, uri: uri)
repo.push
qbash("
set -e
cd '#{Shellwords.escape(path)}'
git checkout -b temp
git branch -D master
git checkout --orphan master
echo 'hello, dude!' > new.txt
git add new.txt
git commit --no-verify --quiet -am 'new master'
")
repo.push
assert_path_exists(File.join(repo.path, 'new.txt'))
end
end
def test_fail_with_user_error
Dir.mktmpdir 'test' do |d|
path, uri = git(d, 'repo')
repo = GitRepo.new(name: 'yegor256/pdd', dir: d, uri: uri)
repo.push
qbash(
"
set -e
cd '#{Shellwords.escape(path)}'
echo '...\x40todoBad puzzle' > z1.txt
echo '\x40todo #1 Good puzzle' > z2.txt
git add z1.txt z2.txt
git commit --no-verify --quiet --amend --message 'zz'
"
)
repo.push
assert_raises(UserError) do
repo.xml
end
end
end
def test_merge_after_amend
Dir.mktmpdir 'test' do |d|
path, uri = git(d, 'repo')
repo = GitRepo.new(name: 'yegor256/pdd', dir: d, uri: uri)
repo.push
qbash("
set -e
cd '#{Shellwords.escape(path)}'
echo 'hello, dude!' > z.txt
git add z.txt
git commit --no-verify --quiet --amend --message 'new fix'
")
repo.push
assert_path_exists(File.join(repo.path, 'z.txt'))
end
end
def test_merge_after_force_push
Dir.mktmpdir 'test' do |d|
path, uri = git(d, 'repo')
repo = GitRepo.new(name: 'yegor256/pdd', dir: d, uri: uri)
repo.push
qbash("
set -e
cd '#{Shellwords.escape(path)}'
git reset HEAD~2
git reset --hard
git clean -fd
echo 'hello, dude!' >> z.txt && git add z.txt && git commit --no-verify -m ddd
echo 'hello, dude!' >> z.txt && git add z.txt && git commit --no-verify -m ddd
echo 'hello, dude!' >> z.txt && git add z.txt && git commit --no-verify -m ddd
")
repo.push
assert_path_exists(File.join(repo.path, 'z.txt'))
end
end
def test_merge_after_complete_new_master
Dir.mktmpdir 'test' do |d|
path, uri = git(d, 'repo')
repo = GitRepo.new(name: 'yegor256/pdd', dir: d, uri: uri)
repo.push
qbash("
set -e
cd '#{Shellwords.escape(path)}'
git checkout -b temp
git branch -D master
git checkout --orphan master
echo 'hello, new!' >> z.txt && git add z.txt && git commit --no-verify -m ddd
echo 'hello, new!' >> z.txt && git add z.txt && git commit --no-verify -m ddd
echo 'hello, new!' >> z2.txt && git add z2.txt && git commit --no-verify -m ddd
")
repo.push
assert_path_exists(File.join(repo.path, 'z.txt'))
assert_path_exists(File.join(repo.path, 'z2.txt'))
end
end
def test_doesnt_touch_crlf
skip('...')
# I can't reproduce the problem of #125. The code works as it should
# be, however in production it fails due to some issues with CRLF
# in binary files.
# See also: https://stackoverflow.com/questions/46539254
Dir.mktmpdir 'test' do |d|
path, uri = git(d, 'repo')
repo = GitRepo.new(name: 'yegor256/pdd', dir: d, uri: uri)
qbash("
set -e
cd '#{Shellwords.escape(path)}'
git config --local core.autocrlf false
echo -n -e 'Hello, world!\r\nHow are you?' >> crlf.txt \
&& git add . && git commit --no-verify -am crlf.txt
")
repo.push
assert_equal(
"Hello, world!\n\rHow are you?",
File.read(File.join(repo.path, 'crlf.txt'))
)
end
end
def test_push
Dir.mktmpdir 'test' do |d|
_, uri = git(d)
repo = GitRepo.new(name: 'teamed/est', dir: d, uri: uri)
repo.push
repo.push
assert_path_exists(File.join(repo.path, '.git'))
end
end
def test_fetch_puzzles
Dir.mktmpdir 'test' do |d|
_, uri = git(d)
repo = GitRepo.new(name: 'yegor256/0pdd', dir: d, uri: uri)
repo.push
refute_empty(repo.xml.xpath('/puzzles'))
end
end
def test_fetch_config
clean_dir = ''
begin
Dir.mktmpdir 'test' do |d|
clean_dir = d
_, uri = git(d)
repo = GitRepo.new(name: 'yegor256/0pdd', dir: d, uri: uri)
repo.push
assert(repo.config['foo'])
end
rescue Errno::ENOTEMPTY
FileUtils.remove_entry(clean_dir, true)
end
end
private
def git(dir, subdir = 'repo')
qbash("
set -e
cd '#{Shellwords.escape(dir)}'
git init --quiet #{Shellwords.escape(subdir)}
cd #{Shellwords.escape(subdir)}
git config user.email git@0pdd.com
git config user.name 0pdd
echo 'foo: hello' > .0pdd.yml
git add .0pdd.yml
git commit --no-verify --quiet -am 'add line'
echo 'hello, world!' >> z.txt && git add z.txt && git commit --no-verify -am z
echo 'hello, world!' >> z.txt && git add z.txt && git commit --no-verify -am z
echo 'hello, world!' >> z.txt && git add z.txt && git commit --no-verify -am z
echo 'hello, world!' >> z.txt && git add z.txt && git commit --no-verify -am z
echo 'hello, world!' >> z.txt && git add z.txt && git commit --no-verify -am z
")
path = File.join(dir, subdir)
[path, "file://#{path}"]
end
end
================================================
FILE: test/test_github.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'test__helper'
require_relative '../objects/clients/github'
# Github test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestGithub < Minitest::Test
def test_configures_everything_right
github = Github.new.client
assert_equal('0pdd', github.user('0pdd')[:login],
"Real user is #{github.user('0pdd')[:login]}")
end
end
================================================
FILE: test/test_github_invitations.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'test__helper'
require_relative 'fake_github'
require_relative '../objects/invitations/github_invitations'
# GithubInvitations test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestGithubInvitation < Minitest::Test
def test_accepts_organization_invitations
organizations = %w[github google microsoft zerocracy]
orgs = %w[github zerocracy]
github = FakeGithub.new(
memberships: organizations.collect do |org|
{
'state' => orgs.include?(org) ? 'active' : 'pending',
'organization' => {
'login' => org
}
}
end
)
invitations = GithubInvitations.new(github)
invitations.accept_orgs
organizations.map do |org|
assert(
github.organization_memberships.find do |m|
m['state'] == 'active' && m['organization']['login'] == org
end
)
end
end
def test_accepts_repository_invitations
repositories = %w[yegor256/0pdd yegor256/sixnines]
github = FakeGithub.new(
invitations: repositories.enum_for(:each_with_index).collect do |repo, i|
{
'id' => i,
'repository' => {
'name' => repo
}
}
end
)
GithubInvitations.new(github).accept
repositories.map { |repo| assert_includes(github.repositories, repo) }
end
end
================================================
FILE: test/test_github_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
require 'yaml'
require_relative 'test__helper'
require_relative '../objects/tickets/tickets'
# GithubTickets test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestGithubTickets < Minitest::Test
def test_submits_tickets
config = YAML.safe_load(
"
alerts:
github:
- yegor256
- davvd
format:
- short-title
- title-length=30
"
)
repo = object(
name: 'github',
config: config,
head_commit_hash: '123',
master: 'master'
)
require_relative 'fake_github'
vcs = FakeGithub.new(repo: repo)
def vcs.create_issue(data)
@data = data
{ number: 1, html_url: 'url' }
end
class << vcs
attr_accessor :data
end
tickets = Tickets.new(vcs)
tickets.submit(
Nokogiri::XML(
'<puzzle>
<id>23-ab536de</id>
<file>/a/b/c/test.txt</file>
<time>01-01-2019</time>
<author>yegor</author>
<body>привет дорогой друг, как твои дела?</body>
<ticket>123</ticket>
<estimate>30</estimate>
<role>DEV</role>
<lines>1-3</lines>
</puzzle>'
).xpath('/puzzle')
)
assert_equal('привет дорогой друг, как...', vcs.data[:title])
assert(vcs.data[:description].start_with?('The puzzle `23-ab536de` from #123 has'))
end
def test_submits_tickets_log_title
config = YAML.safe_load("\n\n")
repo = object(
name: 'github',
config: config,
head_commit_hash: '123',
master: 'master'
)
require_relative 'fake_github'
vcs = FakeGithub.new(repo: repo)
def vcs.create_issue(data)
@data = data
{ number: 1, html_url: 'url' }
end
class << vcs
attr_accessor :data
end
tickets = Tickets.new(vcs)
tickets.submit(
Nokogiri::XML(
'<puzzle>
<id>55-ab536de</id>
<file>/a/bz.txt</file>
<time>01-05-2019</time>
<author>yegor</author>
<body>как дела? hey, how are you, please see this title!</body>
<ticket>123</ticket>
<estimate>30</estimate>
<role>DEV</role>
<lines>1-3</lines>
</puzzle>'
).xpath('/puzzle')
)
assert_equal(
'bz.txt:1-3: как дела? hey, how are you, please see this...',
vcs.data[:title]
)
assert(vcs.data[:description].start_with?('The puzzle `55-ab536de` from #123 has'))
end
def test_output_estimates_when_it_is_not_zero
config = YAML.safe_load("\n\n")
repo = object(
name: 'github',
config: config,
head_commit_hash: '123',
master: 'master'
)
require_relative 'fake_github'
vcs = FakeGithub.new(repo: repo)
def vcs.create_issue(data)
@data = data
{ number: 1, html_url: 'url' }
end
class << vcs
attr_accessor :data
end
tickets = Tickets.new(vcs)
tickets.submit(
Nokogiri::XML(
'<puzzle>
<id>55-ab536de</id>
<file>/a/bz.txt</file>
<time>01-05-2019</time>
<author>yegor</author>
<body>как дела? hey, how are you, please see this title!</body>
<ticket>123</ticket>
<estimate>10</estimate>
<role>DEV</role>
<lines>1-3</lines>
</puzzle>'
).xpath('/puzzle')
)
assert(vcs.data[:description].start_with?('The puzzle `55-ab536de` from #123 has'))
assert_includes(vcs.data[:description], 'Estimate:')
end
def test_skips_estimate_if_zero
config = YAML.safe_load("\n\n")
repo = object(
name: 'github',
config: config,
head_commit_hash: '123',
master: 'master'
)
require_relative 'fake_github'
vcs = FakeGithub.new(repo: repo)
def vcs.create_issue(data)
@data = data
{ number: 1, html_url: 'url' }
end
class << vcs
attr_accessor :data
end
tickets = Tickets.new(vcs)
tickets.submit(
Nokogiri::XML(
'<puzzle>
<id>55-ab536de</id>
<file>/a/bz.txt</file>
<time>01-05-2019</time>
<author>yegor</author>
<body>как дела? hey, how are you, please see this title!</body>
<ticket>123</ticket>
<estimate>0</estimate>
<role>DEV</role>
<lines>1-3</lines>
</puzzle>'
).xpath('/puzzle')
)
assert(vcs.data[:description].start_with?('The puzzle `55-ab536de` from #123 has'))
refute_includes(vcs.data[:description], 'Estimate:')
end
def test_closes_tickets
config = YAML.safe_load("alerts:\n github:\n - yegor256\n - davvd")
repo = object(
name: 'github',
config: config,
head_commit_hash: '123',
master: 'master'
)
require_relative 'fake_github'
tickets = Tickets.new(FakeGithub.new(repo: repo))
tickets.close(
Nokogiri::XML(
'<puzzle><id>xx</id><issue>1</issue></puzzle>'
).xpath('/puzzle')
)
end
end
================================================
FILE: test/test_gitlab.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'test__helper'
require_relative '../objects/clients/gitlab'
# Github test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestGitlab < Minitest::Test
def test_configures_everything_right
gitlab = GitlabClient.new.client
assert_raises Gitlab::Error::MissingCredentials do
gitlab.user('0pdd')['username']
end
end
end
================================================
FILE: test/test_job.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
require 'tmpdir'
require_relative 'test__helper'
require_relative 'fake_repo'
require_relative 'fake_github'
require_relative 'fake_tickets'
require_relative 'fake_storage'
require_relative '../objects/jobs/job'
require_relative '../objects/storage/safe_storage'
# Job test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestJob < Minitest::Test
def test_simple_scenario
Dir.mktmpdir 'test' do |d|
repo = FakeRepo.new
vcs = FakeGithub.new(repo: repo)
Job.new(
vcs,
SafeStorage.new(FakeStorage.new(d)),
FakeTickets.new
).proceed
end
end
end
================================================
FILE: test/test_job_commiterrors.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'test__helper'
require_relative '../objects/jobs/job_commiterrors'
# JobCommitErrors test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestJobCommitErrors < Minitest::Test
class Stub
attr_reader :name, :reported, :repo
def initialize(repo)
@repo = repo
@name = 'GITHUB'
end
def create_commit_comment(_, text)
@reported = text
end
end
def test_timeout_scenario
job = Object.new
def job.proceed
raise 'Intended to be here'
end
vcs = Stub.new(object(head_commit_hash: '123'))
begin
JobCommitErrors.new(vcs, job).proceed
rescue StandardError => e
refute_nil(e)
end
refute_empty(vcs.reported)
end
end
================================================
FILE: test/test_job_detached.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'test__helper'
require_relative '../objects/jobs/job_detached'
# JobDetached test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestJobDetached < Minitest::Test
def test_simple_scenario
job = Object.new
def job.proceed
# nothing
end
require_relative 'fake_repo'
vcs = object(repo: nil)
vcs.repo = FakeRepo.new
JobDetached.new(vcs, job).proceed
end
end
================================================
FILE: test/test_job_emailed.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'veil'
require_relative '../objects/jobs/job_emailed'
require_relative 'fake_github'
require_relative 'fake_repo'
require_relative 'test__helper'
# JobEmailed test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestJobEmailed < Minitest::Test
def fake_job
Veil.new(Object.new, proceed: nil)
end
def test_simple_scenario
repo = FakeRepo.new
vcs = FakeGithub.new(repo: repo)
job = fake_job
JobEmailed.new(vcs, job).proceed
end
def test_exception_mail_to_repo_owner_as_cc
skip('this test needs proper mocking')
repo = FakeRepo.new
vcs = FakeGithub.new(repo: repo)
job = fake_job
assert_raises(StandardError) do
JobEmailed.new(vcs, job).proceed
end
end
end
================================================
FILE: test/test_log.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
require 'tmpdir'
require_relative 'test__helper'
require_relative '../objects/log'
require_relative '../objects/dynamo'
# Log test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestLog < Minitest::Test
def test_put_and_check
log = Log.new(Dynamo.new.aws, 'yegor256/0pdd')
tag = 'some-tag'
log.put(tag, 'some text here')
assert(log.exists(tag))
end
end
================================================
FILE: test/test_logged_storage.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require_relative 'test__helper'
require_relative 'fake_storage'
require_relative 'fake_log'
require_relative '../objects/storage/logged_storage'
require_relative '../objects/storage/versioned_storage'
# LoggedStorage test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestLoggedStorage < Minitest::Test
def test_simple_xml_saving
storage = LoggedStorage.new(
VersionedStorage.new(FakeStorage.new, '0.0.1'), FakeLog.new
)
storage.save(Nokogiri::XML('<test>hello</test>'))
assert_equal('hello', storage.load.xpath('/test/text()')[0].text)
end
end
================================================
FILE: test/test_logged_tickets.rb
================================================
# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko
# SPDX-License-Identifier: MIT
require 'nokogiri'
require 'yaml'
require_relative 'test__helper'
require_relative 'fake_log'
require_relative 'fake_tickets'
require_relative '../objects/tickets/logged_tickets'
# LoggedTickets test.
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2016-2026 Yegor Bugayenko
# License:: MIT
class TestLoggedTickets < Minitest::Test
def test_submits_tickets
log = FakeLog.new
tickets = LoggedTickets.new('yegor256/0pdd', log, FakeTickets.new)
tickets.submit(
Nokogiri::XML(
'<puzzle>
<id>23-ab536de</id>
<file>/a/b/c/test.txt</file>
<body>hey!</body>
<lines>1-3</lines>
</puzzle>'
).xpath('/puzzle')
)
assert_equal('23-ab536de/submit', log.tag)
assert_equal(
'23-ab536de submitted in issue #123: "hey!" at /a/b/c/test.txt; 1-3',
log.title
)
end
def test_closes_tickets
log = FakeLog.new
tickets = LoggedTickets.new('yegor256/0pdd', log, FakeTickets.new)
tickets.close(
Nokogiri::XML(
'<puzzle>
<id>23-ab536fe</id>
<issue>1</issue>
<
gitextract_fz2x5scs/
├── .0pdd.yml
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── actionlint.yml
│ ├── bashate.yml
│ ├── codecov.yml
│ ├── copyrights.yml
│ ├── markdown-lint.yml
│ ├── pdd.yml
│ ├── plantuml.yml
│ ├── rake.yml
│ ├── reuse.yml
│ ├── shellcheck.yml
│ ├── typos.yml
│ ├── xcop.yml
│ └── yamllint.yml
├── .gitignore
├── .pdd
├── .rubocop.yml
├── .rultor.yml
├── 0pdd.rb
├── Aptfile
├── Gemfile
├── LICENSE.txt
├── LICENSES/
│ └── MIT.txt
├── Procfile
├── README.md
├── REUSE.toml
├── Rakefile
├── app.json
├── assets/
│ ├── sass/
│ │ └── main.sass
│ ├── upgrades/
│ │ ├── add-namespace.xsl
│ │ └── remove-broken-issues.xsl
│ ├── xsd/
│ │ └── puzzles.xsd
│ └── xsl/
│ ├── group.xsl
│ ├── join.xsl
│ ├── puzzles.xsl
│ ├── svg.xsl
│ ├── to-close.xsl
│ └── to-submit.xsl
├── config.ru
├── cucumber.yml
├── deploy.sh
├── dynamodb-local/
│ ├── config/
│ │ └── dynamo.yml
│ ├── pom.xml
│ └── tables/
│ └── 0pdd-events.json
├── features/
│ └── step_definitions/
│ └── steps.rb
├── model/
│ ├── README.md
│ ├── fake_weights_storage.rb
│ ├── linear.rb
│ ├── predictor.rb
│ ├── pso/
│ │ ├── lib/
│ │ │ ├── function.rb
│ │ │ ├── functions/
│ │ │ │ ├── rastrigin.rb
│ │ │ │ └── schwefel.rb
│ │ │ ├── solver.rb
│ │ │ ├── version.rb
│ │ │ └── zero_vector.rb
│ │ └── pso.rb
│ └── storage.rb
├── nginx.conf.sigil
├── objects/
│ ├── clients/
│ │ ├── github.rb
│ │ ├── gitlab.rb
│ │ └── jira.rb
│ ├── diff.rb
│ ├── dynamo.rb
│ ├── git_repo.rb
│ ├── invitations/
│ │ ├── github_invitations.rb
│ │ └── github_organization_invitations.rb
│ ├── jobs/
│ │ ├── job.rb
│ │ ├── job_commiterrors.rb
│ │ ├── job_detached.rb
│ │ ├── job_emailed.rb
│ │ ├── job_recorded.rb
│ │ └── job_starred.rb
│ ├── log.rb
│ ├── maybe_text.rb
│ ├── puzzles.rb
│ ├── storage/
│ │ ├── cached_storage.rb
│ │ ├── logged_storage.rb
│ │ ├── once_storage.rb
│ │ ├── s3.rb
│ │ ├── safe_storage.rb
│ │ ├── sync_storage.rb
│ │ ├── upgraded_storage.rb
│ │ └── versioned_storage.rb
│ ├── templates/
│ │ ├── github_tickets_body.haml
│ │ ├── gitlab_tickets_body.haml
│ │ └── jira_tickets_body.haml
│ ├── tickets/
│ │ ├── commit_tickets.rb
│ │ ├── emailed_tickets.rb
│ │ ├── logged_tickets.rb
│ │ ├── milestone_tickets.rb
│ │ ├── sentry_tickets.rb
│ │ ├── tagged_tickets.rb
│ │ └── tickets.rb
│ ├── truncated.rb
│ ├── user_error.rb
│ └── vcs/
│ ├── github.rb
│ ├── gitlab.rb
│ └── jira.rb
├── renovate.json
├── test/
│ ├── fake_github.rb
│ ├── fake_gitlab.rb
│ ├── fake_log.rb
│ ├── fake_repo.rb
│ ├── fake_storage.rb
│ ├── fake_tickets.rb
│ ├── test_0pdd.rb
│ ├── test__helper.rb
│ ├── test_cached_storage.rb
│ ├── test_commit_tickets.rb
│ ├── test_credentials.rb
│ ├── test_diff.rb
│ ├── test_diff_complicated.rb
│ ├── test_git_repo.rb
│ ├── test_github.rb
│ ├── test_github_invitations.rb
│ ├── test_github_tickets.rb
│ ├── test_gitlab.rb
│ ├── test_job.rb
│ ├── test_job_commiterrors.rb
│ ├── test_job_detached.rb
│ ├── test_job_emailed.rb
│ ├── test_log.rb
│ ├── test_logged_storage.rb
│ ├── test_logged_tickets.rb
│ ├── test_maybe_text.rb
│ ├── test_milestone_tickets.rb
│ ├── test_once_storage.rb
│ ├── test_puzzles.rb
│ ├── test_safe_storage.rb
│ ├── test_sentry_tickets.rb
│ ├── test_svg.rb
│ ├── test_truncated.rb
│ ├── test_upgraded_storage.rb
│ └── test_versioned_storage.rb
├── test-assets/
│ └── puzzles/
│ ├── closes-one-puzzle.xml
│ ├── ignores-unknown-issues.xml
│ ├── notify-unknown-open-issues.xml
│ ├── simple.xml
│ ├── submits-old-puzzles.xml
│ ├── submits-ranked-puzzles.xml
│ └── submits-three-tickets.xml
├── version.rb
└── views/
├── _footer.haml
├── _header.haml
├── error.haml
├── error_400.haml
├── index.haml
├── item.haml
├── layout.haml
├── log.haml
└── not_found.haml
SYMBOL INDEX (476 symbols across 84 files)
FILE: 0pdd.rb
function repo_name (line 444) | def repo_name(name)
function vcs_name (line 450) | def vcs_name(name)
function merged (line 455) | def merged(hash)
function storage (line 461) | def storage(repo, vcs)
function process_request (line 494) | def process_request(vcs)
FILE: model/fake_weights_storage.rb
class FakeWeightsStorage (line 7) | class FakeWeightsStorage
method initialize (line 8) | def initialize(
method load (line 15) | def load
method save (line 21) | def save(weights)
FILE: model/linear.rb
class LinearModel (line 17) | class LinearModel
method initialize (line 18) | def initialize(repo, storage)
method predict (line 44) | def predict(puzzles)
method replace_nil (line 65) | def replace_nil(arr, with = 0)
method get_features_labels (line 69) | def get_features_labels(samples)
method extract_features (line 88) | def extract_features(puzzles, samples = {}, level = 1)
method train (line 119) | def train(clf)
method naive_rank (line 136) | def naive_rank(puzzles)
FILE: model/predictor.rb
function argsort (line 6) | def argsort(arr)
function normalised_kendall_tau_distance (line 10) | def normalised_kendall_tau_distance(a, b)
function default_option_generator_linear (line 24) | def default_option_generator_linear(attribute_num)
class Predictor (line 34) | class Predictor
method initialize (line 35) | def initialize(**options)
method f (line 43) | def f(weights, **options)
method train (line 57) | def train(weights, data, true_order)
method predict (line 62) | def predict(weights, data)
method forward_one (line 72) | def forward_one(weights, data)
method kendall (line 79) | def kendall(weights, data, true_order)
FILE: model/pso/lib/function.rb
type Pso (line 6) | module Pso
class Function (line 10) | class Function
method f (line 11) | def f(vector, **_options)
FILE: model/pso/lib/functions/rastrigin.rb
type Pso (line 7) | module Pso
class Rastrigin (line 11) | class Rastrigin < Pso::Function
method f (line 12) | def f(vector, **_options)
FILE: model/pso/lib/functions/schwefel.rb
type Pso (line 7) | module Pso
class Schwefel (line 11) | class Schwefel < Pso::Function
method f (line 12) | def f(vector, **_options)
FILE: model/pso/lib/solver.rb
type Pso (line 8) | module Pso
class Solver (line 12) | class Solver
method initialize (line 13) | def initialize(
method generate_swarm (line 37) | def generate_swarm
method generate_random_noise_particle (line 44) | def generate_random_noise_particle
method generate_random_particle (line 48) | def generate_random_particle
method perfect_particle (line 52) | def perfect_particle
method solve (line 64) | def solve(precision: 100, threads: 1, debug: false)
method best? (line 90) | def best?(best, now)
method normalize (line 98) | def normalize(vector)
method iterate (line 103) | def iterate(vector, best, perfect, speed)
FILE: model/pso/lib/version.rb
type Pso (line 4) | module Pso
FILE: model/pso/lib/zero_vector.rb
class ZeroVector (line 9) | class ZeroVector < Vector
method normalize (line 10) | def normalize
FILE: model/pso/pso.rb
type Pso (line 10) | module Pso
FILE: model/storage.rb
class Storage (line 11) | class Storage
method initialize (line 12) | def initialize(ocket, bucket, region, key, secret)
method load (line 19) | def load
method save (line 28) | def save(weights)
FILE: objects/clients/github.rb
class Github (line 10) | class Github
method initialize (line 11) | def initialize(config = {})
method client (line 15) | def client
FILE: objects/clients/gitlab.rb
class GitlabClient (line 10) | class GitlabClient
method initialize (line 11) | def initialize(config = {})
method client (line 15) | def client
FILE: objects/clients/jira.rb
class JiraClient (line 11) | class JiraClient
method initialize (line 12) | def initialize(config = {})
method client (line 16) | def client
FILE: objects/diff.rb
class Diff (line 9) | class Diff
method initialize (line 10) | def initialize(before, after)
method notify (line 15) | def notify(tickets)
method issues (line 27) | def issues(xml, *xpath)
method summary (line 41) | def summary(xml, ticket)
FILE: objects/dynamo.rb
class Dynamo (line 10) | class Dynamo
method initialize (line 11) | def initialize(config = {})
method aws (line 15) | def aws
FILE: objects/git_repo.rb
class GitRepo (line 17) | class GitRepo
method initialize (line 20) | def initialize(
method lock (line 38) | def lock
method config (line 42) | def config
method xml (line 51) | def xml
method push (line 63) | def push
method change_in_master? (line 71) | def change_in_master?
method clone (line 77) | def clone
method pull (line 83) | def pull
method prepare_key (line 101) | def prepare_key
method prepare_git (line 116) | def prepare_git
FILE: objects/invitations/github_invitations.rb
class GithubInvitations (line 7) | class GithubInvitations
method initialize (line 8) | def initialize(github)
method accept (line 12) | def accept
method accept_single_invitation (line 19) | def accept_single_invitation(repo)
method accept_orgs (line 27) | def accept_orgs
FILE: objects/invitations/github_organization_invitations.rb
class GithubOrganizationInvitations (line 9) | class GithubOrganizationInvitations
method initialize (line 10) | def initialize(github)
method all (line 14) | def all
FILE: objects/jobs/job.rb
class Job (line 11) | class Job
method initialize (line 12) | def initialize(vcs, storage, tickets)
method proceed (line 18) | def proceed
method opts (line 28) | def opts
FILE: objects/jobs/job_commiterrors.rb
class JobCommitErrors (line 9) | class JobCommitErrors
method initialize (line 10) | def initialize(vcs, job)
method proceed (line 15) | def proceed
FILE: objects/jobs/job_detached.rb
class JobDetached (line 9) | class JobDetached
method initialize (line 10) | def initialize(vcs, job)
method proceed (line 15) | def proceed
method exclusive (line 25) | def exclusive
FILE: objects/jobs/job_emailed.rb
class JobEmailed (line 9) | class JobEmailed
method initialize (line 10) | def initialize(vcs, job)
method proceed (line 15) | def proceed
method repo_user_login (line 62) | def repo_user_login
method user_email (line 66) | def user_email(username)
FILE: objects/jobs/job_recorded.rb
class JobRecorded (line 7) | class JobRecorded
method initialize (line 8) | def initialize(vcs, job)
method proceed (line 13) | def proceed
FILE: objects/jobs/job_starred.rb
class JobStarred (line 8) | class JobStarred
method initialize (line 9) | def initialize(vcs, job)
method proceed (line 14) | def proceed
FILE: objects/log.rb
class Log (line 13) | class Log
method initialize (line 14) | def initialize(dynamo, repo, vcs = 'github')
method put (line 24) | def put(tag, text)
method get (line 37) | def get(tag)
method exists (line 51) | def exists(tag)
method delete (line 65) | def delete(time, tag)
method list (line 79) | def list(since = Time.now.to_i)
FILE: objects/maybe_text.rb
class MaybeText (line 7) | class MaybeText
method initialize (line 8) | def initialize(text_if_present, maybe, exclude_if: false)
method to_s (line 14) | def to_s
FILE: objects/puzzles.rb
class Puzzles (line 17) | class Puzzles
method initialize (line 18) | def initialize(repo, storage)
method deploy (line 28) | def deploy(tickets)
method save (line 38) | def save(xml)
method join (line 47) | def join(before, snapshot)
method group (line 60) | def group(xml)
method expose (line 68) | def expose(xml, tickets)
FILE: objects/storage/cached_storage.rb
class CachedStorage (line 7) | class CachedStorage
method initialize (line 8) | def initialize(origin, file)
method load (line 13) | def load
method save (line 28) | def save(xml)
method write (line 36) | def write(xml)
FILE: objects/storage/logged_storage.rb
class LoggedStorage (line 7) | class LoggedStorage
method initialize (line 8) | def initialize(origin, log)
method load (line 13) | def load
method save (line 17) | def save(xml)
FILE: objects/storage/once_storage.rb
class OnceStorage (line 7) | class OnceStorage
method initialize (line 8) | def initialize(origin)
method load (line 12) | def load
method save (line 16) | def save(xml)
FILE: objects/storage/s3.rb
class S3 (line 11) | class S3
method initialize (line 12) | def initialize(ocket, bucket, region, key, secret)
method load (line 19) | def load
method save (line 33) | def save(xml)
FILE: objects/storage/safe_storage.rb
class SafeStorage (line 9) | class SafeStorage
method initialize (line 10) | def initialize(origin)
method load (line 15) | def load
method save (line 19) | def save(xml)
method valid (line 25) | def valid(xml)
FILE: objects/storage/sync_storage.rb
class SyncStorage (line 7) | class SyncStorage
method initialize (line 8) | def initialize(origin)
method load (line 13) | def load
method save (line 17) | def save(xml)
FILE: objects/storage/upgraded_storage.rb
class UpgradedStorage (line 7) | class UpgradedStorage
method initialize (line 8) | def initialize(origin, version)
method load (line 13) | def load
method save (line 26) | def save(xml)
FILE: objects/storage/versioned_storage.rb
class VersionedStorage (line 7) | class VersionedStorage
method initialize (line 8) | def initialize(origin, version)
method load (line 13) | def load
method save (line 23) | def save(xml)
FILE: objects/tickets/commit_tickets.rb
class CommitTickets (line 7) | class CommitTickets
method initialize (line 8) | def initialize(vcs, tickets)
method notify (line 14) | def notify(issue, message)
method submit (line 18) | def submit(puzzle)
method close (line 33) | def close(puzzle)
method opts (line 51) | def opts
method suppressed_repo? (line 56) | def suppressed_repo?
FILE: objects/tickets/emailed_tickets.rb
class EmailedTickets (line 7) | class EmailedTickets
method initialize (line 8) | def initialize(vcs, tickets)
method notify (line 13) | def notify(issue, message)
method submit (line 17) | def submit(puzzle)
method close (line 46) | def close(puzzle)
FILE: objects/tickets/logged_tickets.rb
class LoggedTickets (line 11) | class LoggedTickets
method initialize (line 12) | def initialize(vcs, log, tickets)
method notify (line 18) | def notify(issue, message)
method submit (line 22) | def submit(puzzle)
method close (line 43) | def close(puzzle)
FILE: objects/tickets/milestone_tickets.rb
class MilestoneTickets (line 7) | class MilestoneTickets
method initialize (line 8) | def initialize(vcs, tickets)
method notify (line 13) | def notify(issue, message)
method submit (line 17) | def submit(puzzle)
method close (line 54) | def close(puzzle)
FILE: objects/tickets/sentry_tickets.rb
class SentryTickets (line 12) | class SentryTickets
method initialize (line 13) | def initialize(tickets)
method notify (line 17) | def notify(issue, message)
method submit (line 27) | def submit(puzzle)
method close (line 38) | def close(puzzle)
method email (line 51) | def email(e)
FILE: objects/tickets/tagged_tickets.rb
class TaggedTickets (line 7) | class TaggedTickets
method initialize (line 8) | def initialize(vcs, tickets)
method notify (line 13) | def notify(issue, message)
method submit (line 17) | def submit(puzzle)
method close (line 54) | def close(puzzle)
FILE: objects/tickets/tickets.rb
class Tickets (line 11) | class Tickets
method initialize (line 12) | def initialize(vcs)
method notify (line 16) | def notify(issue, message)
method submit (line 25) | def submit(puzzle)
method close (line 37) | def close(puzzle)
method users (line 54) | def users
method title (line 67) | def title(puzzle)
method body (line 89) | def body(puzzle)
FILE: objects/truncated.rb
class Truncated (line 7) | class Truncated
method initialize (line 8) | def initialize(text, max = 40, tail = '...')
method to_s (line 14) | def to_s
FILE: objects/user_error.rb
class UserError (line 7) | class UserError < StandardError
FILE: objects/vcs/github.rb
class GithubRepo (line 10) | class GithubRepo
method initialize (line 13) | def initialize(client, json, config = {})
method exists? (line 25) | def exists?
method issue (line 35) | def issue(issue_id)
method close_issue (line 52) | def close_issue(issue_id)
method create_issue (line 56) | def create_issue(data)
method update_issue (line 67) | def update_issue(issue_id, data)
method labels (line 71) | def labels
method add_label (line 75) | def add_label(label, color)
method add_labels_to_an_issue (line 79) | def add_labels_to_an_issue(issue_id, labels)
method add_comment (line 83) | def add_comment(issue_id, comment)
method create_commit_comment (line 87) | def create_commit_comment(sha, comment)
method list_commits (line 91) | def list_commits
method user (line 95) | def user(username)
method star (line 99) | def star
method repository_link (line 103) | def repository_link
method collaborators_link (line 107) | def collaborators_link
method file_link (line 111) | def file_link(file)
method puzzle_link_for_commit (line 115) | def puzzle_link_for_commit(sha, file, start, stop)
method issue_link (line 119) | def issue_link(issue_id)
method git_repo (line 125) | def git_repo(json, config)
FILE: objects/vcs/gitlab.rb
class GitlabRepo (line 12) | class GitlabRepo
method initialize (line 15) | def initialize(client, json, config = {})
method issue (line 23) | def issue(issue_id)
method close_issue (line 41) | def close_issue(issue_id)
method create_issue (line 47) | def create_issue(data)
method update_issue (line 56) | def update_issue(issue_id, data)
method labels (line 60) | def labels
method add_label (line 73) | def add_label(label, color)
method add_labels_to_an_issue (line 77) | def add_labels_to_an_issue(issue_id, labels)
method add_comment (line 82) | def add_comment(issue_id, comment)
method create_commit_comment (line 88) | def create_commit_comment(sha, comment)
method list_commits (line 97) | def list_commits
method user (line 107) | def user(username)
method star (line 116) | def star
method exists? (line 120) | def exists?
method repository_link (line 135) | def repository_link
method collaborators_link (line 139) | def collaborators_link
method file_link (line 143) | def file_link(file)
method puzzle_link_for_commit (line 147) | def puzzle_link_for_commit(sha, file, start, stop)
method issue_link (line 151) | def issue_link(issue_id)
method git_repo (line 157) | def git_repo(json, config)
FILE: objects/vcs/jira.rb
class JiraRepo (line 10) | class JiraRepo
method initialize (line 13) | def initialize(client, json, config = {})
method issue (line 21) | def issue(issue_id)
method close_issue (line 25) | def close_issue(issue_id)
method create_issue (line 38) | def create_issue(data)
method update_issue (line 50) | def update_issue(issue_id, data)
method exists? (line 62) | def exists?
method repository_link (line 70) | def repository_link
method git_repo (line 76) | def git_repo(json, config)
FILE: test/fake_github.rb
class FakeGithub (line 4) | class FakeGithub
method initialize (line 7) | def initialize(options = {})
method rate_limit (line 39) | def rate_limit
method update_organization_membership (line 48) | def update_organization_membership(org, options = {})
method organization_memberships (line 55) | def organization_memberships(options = {})
method user_repository_invitations (line 63) | def user_repository_invitations(_options = {})
method accept_repository_invitation (line 67) | def accept_repository_invitation(id, _options = {})
method repositories (line 74) | def repositories(user = nil, _options = {})
method issue (line 78) | def issue(_)
method close_issue (line 92) | def close_issue(_); end
method create_issue (line 94) | def create_issue(_)
method update_issue (line 101) | def update_issue(_, _); end
method labels (line 103) | def labels
method add_label (line 113) | def add_label(_, _); end
method add_labels_to_an_issue (line 115) | def add_labels_to_an_issue(_, _); end
method add_comment (line 117) | def add_comment(_, _); end
method create_commit_comment (line 119) | def create_commit_comment(_, _, _)
method list_commits (line 125) | def list_commits
method user (line 134) | def user(_)
method star (line 141) | def star; end
method repository (line 143) | def repository(_ = nil)
method repository_link (line 149) | def repository_link
method collaborators_link (line 153) | def collaborators_link
method file_link (line 157) | def file_link(file)
method puzzle_link_for_commit (line 161) | def puzzle_link_for_commit(sha, file, start, stop)
method issue_link (line 165) | def issue_link(issue_id)
method git_repo (line 171) | def git_repo
FILE: test/fake_gitlab.rb
class FakeGitlab (line 4) | class FakeGitlab
method initialize (line 7) | def initialize(options = {})
method repositories (line 14) | def repositories(user = nil, _options = {})
method issue (line 18) | def issue(_)
method close_issue (line 32) | def close_issue(_); end
method create_issue (line 34) | def create_issue(_)
method update_issue (line 41) | def update_issue(_, _); end
method labels (line 43) | def labels
method add_label (line 53) | def add_label(_, _); end
method add_labels_to_an_issue (line 55) | def add_labels_to_an_issue(_, _); end
method add_comment (line 57) | def add_comment(_, _); end
method create_commit_comment (line 59) | def create_commit_comment(_, _)
method list_commits (line 65) | def list_commits
method user (line 74) | def user(_)
method star (line 81) | def star; end
method repository (line 83) | def repository(_ = nil)
method project (line 89) | def project(_ = nil)
method repository_link (line 95) | def repository_link
method collaborators_link (line 99) | def collaborators_link
method file_link (line 103) | def file_link(file)
method puzzle_link_for_commit (line 107) | def puzzle_link_for_commit(sha, file, start, stop)
method issue_link (line 111) | def issue_link(issue_id)
method git_repo (line 117) | def git_repo
FILE: test/fake_log.rb
class FakeLog (line 4) | class FakeLog
method exists (line 7) | def exists(_)
method put (line 11) | def put(tag, text)
method get (line 16) | def get(_tag); end
method delete (line 18) | def delete(_time, _tag); end
method list (line 20) | def list(_since = Time.now.to_i)
FILE: test/fake_repo.rb
class FakeRepo (line 7) | class FakeRepo
method initialize (line 10) | def initialize(options = {})
method lock (line 15) | def lock
method xml (line 19) | def xml
method push (line 23) | def push
FILE: test/fake_storage.rb
class FakeStorage (line 7) | class FakeStorage
method initialize (line 8) | def initialize(
method load (line 16) | def load
method save (line 20) | def save(xml)
FILE: test/fake_tickets.rb
class FakeTickets (line 4) | class FakeTickets
method initialize (line 7) | def initialize
method submit (line 12) | def submit(puzzle)
method close (line 17) | def close(puzzle)
FILE: test/test_0pdd.rb
class AppTest (line 8) | class AppTest < Minitest::Test
method app (line 11) | def app
method test_renders_version (line 15) | def test_renders_version
method test_robots_txt (line 20) | def test_robots_txt
method test_it_renders_home_page (line 25) | def test_it_renders_home_page
method test_renders_some_pages (line 31) | def test_renders_some_pages
method test_it_renders_puzzles_xsd (line 45) | def test_it_renders_puzzles_xsd
method test_renders_log_page (line 51) | def test_renders_log_page
method test_renders_log_item (line 61) | def test_renders_log_item
method test_renders_page_not_found (line 72) | def test_renders_page_not_found
method test_it_understands_push_from_github (line 77) | def test_it_understands_push_from_github
method test_it_ignores_push_from_github_to_not_master (line 95) | def test_it_ignores_push_from_github_to_not_master
method test_it_accepts_push_from_github_to_not_default_master (line 114) | def test_it_accepts_push_from_github_to_not_default_master
method test_it_ignore_push_from_github_to_not_default_master (line 134) | def test_it_ignore_push_from_github_to_not_default_master
method test_it_understands_push_from_gitlab (line 154) | def test_it_understands_push_from_gitlab
method test_it_ignores_push_from_gitlab_to_not_master (line 172) | def test_it_ignores_push_from_gitlab_to_not_master
method test_it_accepts_push_from_gitlab_to_not_default_master (line 191) | def test_it_accepts_push_from_gitlab_to_not_default_master
method test_it_ignores_push_from_gitlab_to_not_default_master (line 211) | def test_it_ignores_push_from_gitlab_to_not_default_master
method test_renders_html_puzzles (line 231) | def test_renders_html_puzzles
method test_snapshots_unavailable_repo (line 242) | def test_snapshots_unavailable_repo
method test_renders_svg_puzzles (line 247) | def test_renders_svg_puzzles
method test_renders_xml_puzzles (line 258) | def test_renders_xml_puzzles
method test_rejects_invalid_repo_name (line 268) | def test_rejects_invalid_repo_name
method test_not_found (line 273) | def test_not_found
FILE: test/test__helper.rb
function object (line 33) | def object(hash)
FILE: test/test_cached_storage.rb
class TestCachedStorage (line 12) | class TestCachedStorage < Minitest::Test
method test_simple_xml_loading (line 13) | def test_simple_xml_loading
FILE: test/test_commit_tickets.rb
class TestCommitTickets (line 12) | class TestCommitTickets < Minitest::Test
method test_submits_tickets (line 13) | def test_submits_tickets
method test_closes_tickets (line 29) | def test_closes_tickets
method test_scope_suppressed_repo_should_be_quiet (line 45) | def test_scope_suppressed_repo_should_be_quiet
FILE: test/test_credentials.rb
class CredentialsTest (line 16) | class CredentialsTest < Minitest::Test
method test_connects_to_git_via_ssh (line 17) | def test_connects_to_git_via_ssh
method test_connects_to_aws_dynamo (line 31) | def test_connects_to_aws_dynamo
method test_connects_to_github (line 41) | def test_connects_to_github
method test_connects_to_aws_s3 (line 69) | def test_connects_to_aws_s3
method test_sends_email_via_smtp (line 81) | def test_sends_email_via_smtp
method config (line 108) | def config
FILE: test/test_diff.rb
class TestDiff (line 13) | class TestDiff < Minitest::Test
method test_notification_on_one_new_puzzle (line 14) | def test_notification_on_one_new_puzzle
method test_notification_unknown_issue (line 46) | def test_notification_unknown_issue
method test_notification_on_two_new_puzzles (line 62) | def test_notification_on_two_new_puzzles
method test_notification_on_solved_puzzle (line 101) | def test_notification_on_solved_puzzle
method test_notification_on_one_solved_puzzle (line 125) | def test_notification_on_one_solved_puzzle
method test_notification_on_update (line 161) | def test_notification_on_update
method test_quiet_when_no_changes (line 191) | def test_quiet_when_no_changes
class Tickets (line 214) | class Tickets
method initialize (line 217) | def initialize
method notify (line 221) | def notify(ticket, text)
FILE: test/test_diff_complicated.rb
class TestDiff (line 10) | class TestDiff < Minitest::Test
method test_notification_on_parent_solved_with_others_unsolved (line 16) | def test_notification_on_parent_solved_with_others_unsolved
class Tickets (line 52) | class Tickets
method initialize (line 55) | def initialize
method notify (line 59) | def notify(ticket, text)
FILE: test/test_git_repo.rb
class TestGitRepo (line 13) | class TestGitRepo < Minitest::Test
method test_clone_and_pull (line 14) | def test_clone_and_pull
method test_merge_unrelated_histories (line 24) | def test_merge_unrelated_histories
method test_fail_with_user_error (line 44) | def test_fail_with_user_error
method test_merge_after_amend (line 66) | def test_merge_after_amend
method test_merge_after_force_push (line 83) | def test_merge_after_force_push
method test_merge_after_complete_new_master (line 103) | def test_merge_after_complete_new_master
method test_doesnt_touch_crlf (line 124) | def test_doesnt_touch_crlf
method test_push (line 148) | def test_push
method test_fetch_puzzles (line 158) | def test_fetch_puzzles
method test_fetch_config (line 167) | def test_fetch_config
method git (line 184) | def git(dir, subdir = 'repo')
FILE: test/test_github.rb
class TestGithub (line 11) | class TestGithub < Minitest::Test
method test_configures_everything_right (line 12) | def test_configures_everything_right
FILE: test/test_github_invitations.rb
class TestGithubInvitation (line 12) | class TestGithubInvitation < Minitest::Test
method test_accepts_organization_invitations (line 13) | def test_accepts_organization_invitations
method test_accepts_repository_invitations (line 37) | def test_accepts_repository_invitations
FILE: test/test_github_tickets.rb
class TestGithubTickets (line 13) | class TestGithubTickets < Minitest::Test
method test_submits_tickets (line 14) | def test_submits_tickets
method test_submits_tickets_log_title (line 61) | def test_submits_tickets_log_title
method test_output_estimates_when_it_is_not_zero (line 101) | def test_output_estimates_when_it_is_not_zero
method test_skips_estimate_if_zero (line 138) | def test_skips_estimate_if_zero
method test_closes_tickets (line 175) | def test_closes_tickets
FILE: test/test_gitlab.rb
class TestGitlab (line 11) | class TestGitlab < Minitest::Test
method test_configures_everything_right (line 12) | def test_configures_everything_right
FILE: test/test_job.rb
class TestJob (line 18) | class TestJob < Minitest::Test
method test_simple_scenario (line 19) | def test_simple_scenario
FILE: test/test_job_commiterrors.rb
class TestJobCommitErrors (line 11) | class TestJobCommitErrors < Minitest::Test
class Stub (line 12) | class Stub
method initialize (line 15) | def initialize(repo)
method create_commit_comment (line 20) | def create_commit_comment(_, text)
method test_timeout_scenario (line 25) | def test_timeout_scenario
FILE: test/test_job_detached.rb
class TestJobDetached (line 11) | class TestJobDetached < Minitest::Test
method test_simple_scenario (line 12) | def test_simple_scenario
FILE: test/test_job_emailed.rb
class TestJobEmailed (line 14) | class TestJobEmailed < Minitest::Test
method fake_job (line 15) | def fake_job
method test_simple_scenario (line 19) | def test_simple_scenario
method test_exception_mail_to_repo_owner_as_cc (line 26) | def test_exception_mail_to_repo_owner_as_cc
FILE: test/test_log.rb
class TestLog (line 14) | class TestLog < Minitest::Test
method test_put_and_check (line 15) | def test_put_and_check
FILE: test/test_logged_storage.rb
class TestLoggedStorage (line 14) | class TestLoggedStorage < Minitest::Test
method test_simple_xml_saving (line 15) | def test_simple_xml_saving
FILE: test/test_logged_tickets.rb
class TestLoggedTickets (line 15) | class TestLoggedTickets < Minitest::Test
method test_submits_tickets (line 16) | def test_submits_tickets
method test_closes_tickets (line 36) | def test_closes_tickets
FILE: test/test_maybe_text.rb
class TestMaybeText (line 8) | class TestMaybeText < Minitest::Test
method test_nil_input_then_blank (line 9) | def test_nil_input_then_blank
method test_empty_input_then_blank (line 13) | def test_empty_input_then_blank
method test_excluded_input_then_blank (line 17) | def test_excluded_input_then_blank
method test_present_input_then_output (line 21) | def test_present_input_then_output
method test_show_output_when_exclude_if_is_present (line 25) | def test_show_output_when_exclude_if_is_present
FILE: test/test_milestone_tickets.rb
class TestGithubTickets (line 14) | class TestGithubTickets < Minitest::Test
method test_sets_milestone (line 15) | def test_sets_milestone
method test_does_not_set_milestone (line 60) | def test_does_not_set_milestone
method test_adds_comment (line 102) | def test_adds_comment
FILE: test/test_once_storage.rb
class TestOnceStorage (line 12) | class TestOnceStorage < Minitest::Test
method test_never_saves_duplicates (line 13) | def test_never_saves_duplicates
method test_saves_only_once (line 20) | def test_saves_only_once
class TestStorage (line 27) | class TestStorage
method initialize (line 30) | def initialize
method load (line 34) | def load
method save (line 38) | def save(_)
FILE: test/test_puzzles.rb
class TestPuzzles (line 20) | class TestPuzzles < Minitest::Test
method test_all_xml (line 21) | def test_all_xml
method test_with_broken_tickets (line 32) | def test_with_broken_tickets
method test_xml (line 54) | def test_xml(dir, name, ordered: false)
FILE: test/test_safe_storage.rb
class TestSafeStorage (line 14) | class TestSafeStorage < Minitest::Test
method test_accepts_valid_xml (line 15) | def test_accepts_valid_xml
method test_rejects_invalid_xml (line 42) | def test_rejects_invalid_xml
FILE: test/test_sentry_tickets.rb
class TestSentryTickets (line 12) | class TestSentryTickets < Minitest::Test
method test_exception_catching_on_submit (line 13) | def test_exception_catching_on_submit
method test_exception_catching_on_close (line 23) | def test_exception_catching_on_close
FILE: test/test_svg.rb
class TestSvg (line 8) | class TestSvg < Minitest::Test
method render (line 11) | def render(alive:, dead: 0)
method count_text (line 16) | def count_text(svg)
method badge_width (line 23) | def badge_width(svg)
method test_renders_small_count (line 27) | def test_renders_small_count
method test_renders_count_when_above_threshold (line 33) | def test_renders_count_when_above_threshold
method test_renders_large_count (line 39) | def test_renders_large_count
method test_widens_to_fit_large_numbers (line 45) | def test_widens_to_fit_large_numbers
method test_text_anchor_stays_inside_badge (line 57) | def test_text_anchor_stays_inside_badge
FILE: test/test_truncated.rb
class TestTruncated (line 11) | class TestTruncated < Minitest::Test
method test_simple_formatting (line 12) | def test_simple_formatting
method test_very_long_text (line 16) | def test_very_long_text
method test_short_long_text (line 23) | def test_short_long_text
method test_unicode_text (line 27) | def test_unicode_text
method test_multi_line_text (line 34) | def test_multi_line_text
FILE: test/test_upgraded_storage.rb
class TestUpgradedStorage (line 16) | class TestUpgradedStorage < Minitest::Test
method test_safety_preserved (line 17) | def test_safety_preserved
method test_removes_broken_issues (line 27) | def test_removes_broken_issues
method test_removes_broken_href (line 39) | def test_removes_broken_href
FILE: test/test_versioned_storage.rb
class TestVersionedStorage (line 14) | class TestVersionedStorage < Minitest::Test
method test_xml_versioning (line 15) | def test_xml_versioning
Condensed preview — 152 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (234K chars).
[
{
"path": ".0pdd.yml",
"chars": 194,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\nerrors:\n - yegor25"
},
{
"path": ".gitattributes",
"chars": 243,
"preview": "# Check out all text files in UNIX format, with LF as end of line\n# Don't change this file. If you have any ideas about "
},
{
"path": ".github/workflows/actionlint.yml",
"chars": 675,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/bashate.yml",
"chars": 581,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/codecov.yml",
"chars": 781,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/copyrights.yml",
"chars": 405,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/markdown-lint.yml",
"chars": 421,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/pdd.yml",
"chars": 392,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/plantuml.yml",
"chars": 782,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/rake.yml",
"chars": 710,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/reuse.yml",
"chars": 382,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/shellcheck.yml",
"chars": 404,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/typos.yml",
"chars": 384,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/xcop.yml",
"chars": 383,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".github/workflows/yamllint.yml",
"chars": 394,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": ".gitignore",
"chars": 120,
"preview": "*.gem\n*.iml\n.bundle/\n.claude/\n.DS_Store\n.idea/\n.sass-cache/\n.yardoc/\ncoverage/\ndoc/\nnode_modules/\nrdoc/\ntarget/\nvendor/\n"
},
{
"path": ".pdd",
"chars": 179,
"preview": "--source=.\n--verbose\n--exclude README.md\n--exclude coverage/**/*\n--exclude assets/**/*\n--exclude model/data/**/*\n--rule "
},
{
"path": ".rubocop.yml",
"chars": 1246,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\nAllCops:\n Exclude:"
},
{
"path": ".rultor.yml",
"chars": 1421,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\n# yamllint disable "
},
{
"path": "0pdd.rb",
"chars": 15038,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n$stdout.sync = true\n\nr"
},
{
"path": "Aptfile",
"chars": 4,
"preview": "git\n"
},
{
"path": "Gemfile",
"chars": 1166,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nsource 'https://rubyge"
},
{
"path": "LICENSE.txt",
"chars": 1083,
"preview": "(The MIT License)\n\nCopyright (c) 2016-2026 Yegor Bugayenko\n\nPermission is hereby granted, free of charge, to any person "
},
{
"path": "LICENSES/MIT.txt",
"chars": 1083,
"preview": "(The MIT License)\n\nCopyright (c) 2016-2026 Yegor Bugayenko\n\nPermission is hereby granted, free of charge, to any person "
},
{
"path": "Procfile",
"chars": 90,
"preview": "web: bundle exec rackup config.ru -p $PORT\ncron: curl -s https://www.0pdd.com/ping-github\n"
},
{
"path": "README.md",
"chars": 9491,
"preview": "# Puzzle Driven Development (PDD) GitHub Chatbot\n\n[ 2025 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nversion = 1\n[[annotations]]"
},
{
"path": "Rakefile",
"chars": 1673,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'rubygems'\nreq"
},
{
"path": "app.json",
"chars": 255,
"preview": "{\n \"healthchecks\": {\n \"web\": [\n {\n \"attempts\": 3,\n \"description\": \"Checking if the app responds t"
},
{
"path": "assets/sass/main.sass",
"chars": 464,
"preview": "// SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n// SPDX-License-Identifier: MIT\n\nbody\n background-co"
},
{
"path": "assets/upgrades/add-namespace.xsl",
"chars": 761,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "assets/upgrades/remove-broken-issues.xsl",
"chars": 788,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "assets/xsd/puzzles.xsd",
"chars": 4413,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "assets/xsl/group.xsl",
"chars": 1073,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "assets/xsl/join.xsl",
"chars": 1620,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "assets/xsl/puzzles.xsl",
"chars": 4418,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "assets/xsl/svg.xsl",
"chars": 1990,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "assets/xsl/to-close.xsl",
"chars": 757,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "assets/xsl/to-submit.xsl",
"chars": 861,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "config.ru",
"chars": 162,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire './0pdd'\n\n$std"
},
{
"path": "cucumber.yml",
"chars": 224,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\ndefault: --format p"
},
{
"path": "deploy.sh",
"chars": 465,
"preview": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\ns"
},
{
"path": "dynamodb-local/config/dynamo.yml",
"chars": 166,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n---\nport: ${dynamo.port"
},
{
"path": "dynamodb-local/pom.xml",
"chars": 3756,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "dynamodb-local/tables/0pdd-events.json",
"chars": 972,
"preview": "{\n \"AttributeDefinitions\": [\n {\n \"AttributeName\": \"repo\",\n \"AttributeType\": \"S\"\n },\n {\n \"Attrib"
},
{
"path": "features/step_definitions/steps.rb",
"chars": 295,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'tmpdir'\nrequi"
},
{
"path": "model/README.md",
"chars": 1025,
"preview": "Puzzle Ranking (Linear ML Model)\n\n### Internals\n\nThe ML model is a linear model with PSO optimizer.\nThe optimizer is use"
},
{
"path": "model/fake_weights_storage.rb",
"chars": 491,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# FakeWeightsStorage"
},
{
"path": "model/linear.rb",
"chars": 4682,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'json'\nrequire"
},
{
"path": "model/predictor.rb",
"chars": 1974,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'pso/"
},
{
"path": "model/pso/lib/function.rb",
"chars": 264,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'matrix'\n\nmodu"
},
{
"path": "model/pso/lib/functions/rastrigin.rb",
"chars": 409,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative '../f"
},
{
"path": "model/pso/lib/functions/schwefel.rb",
"chars": 407,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative '../f"
},
{
"path": "model/pso/lib/solver.rb",
"chars": 3248,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'zero"
},
{
"path": "model/pso/lib/version.rb",
"chars": 140,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nmodule Pso\n VERSION ="
},
{
"path": "model/pso/lib/zero_vector.rb",
"chars": 227,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'matrix'\n\n#\n# "
},
{
"path": "model/pso/pso.rb",
"chars": 192,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'lib/"
},
{
"path": "model/storage.rb",
"chars": 821,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'json'\nrequire"
},
{
"path": "nginx.conf.sigil",
"chars": 6506,
"preview": "{{ range $port_map := .PROXY_PORT_MAP | split \" \" }}\n{{ $port_map_list := $port_map | split \":\" }}\n{{ $scheme := index $"
},
{
"path": "objects/clients/github.rb",
"chars": 686,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'octokit'\n\n#\n#"
},
{
"path": "objects/clients/gitlab.rb",
"chars": 642,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'gitlab'\n\n#\n# "
},
{
"path": "objects/clients/jira.rb",
"chars": 799,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'rubygems'\nreq"
},
{
"path": "objects/diff.rb",
"chars": 1722,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\n\n#\n"
},
{
"path": "objects/dynamo.rb",
"chars": 950,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'yaml'\nrequire"
},
{
"path": "objects/git_repo.rb",
"chars": 3129,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'base64'\nrequi"
},
{
"path": "objects/invitations/github_invitations.rb",
"chars": 1279,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Invitations in Git"
},
{
"path": "objects/invitations/github_organization_invitations.rb",
"chars": 452,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'gith"
},
{
"path": "objects/jobs/job.rb",
"chars": 656,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'mail'\nrequire"
},
{
"path": "objects/jobs/job_commiterrors.rb",
"chars": 892,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative '../t"
},
{
"path": "objects/jobs/job_detached.rb",
"chars": 715,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'fileutils'\n\n#"
},
{
"path": "objects/jobs/job_emailed.rb",
"chars": 1825,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'mail'\n\n#\n# Jo"
},
{
"path": "objects/jobs/job_recorded.rb",
"chars": 339,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Job that records a"
},
{
"path": "objects/jobs/job_starred.rb",
"chars": 345,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Job that stars the"
},
{
"path": "objects/log.rb",
"chars": 2218,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'base64'\nrequi"
},
{
"path": "objects/maybe_text.rb",
"chars": 394,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Maybe text\n#\nclass"
},
{
"path": "objects/puzzles.rb",
"chars": 3162,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'json'\nrequire"
},
{
"path": "objects/storage/cached_storage.rb",
"chars": 733,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# XML cached in a te"
},
{
"path": "objects/storage/logged_storage.rb",
"chars": 573,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Storage that is lo"
},
{
"path": "objects/storage/once_storage.rb",
"chars": 338,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Saves only once, i"
},
{
"path": "objects/storage/s3.rb",
"chars": 1009,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'aws-sdk-s3'\nr"
},
{
"path": "objects/storage/safe_storage.rb",
"chars": 572,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\n\n#\n"
},
{
"path": "objects/storage/sync_storage.rb",
"chars": 348,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Thread-safe storag"
},
{
"path": "objects/storage/upgraded_storage.rb",
"chars": 591,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Storage that upgra"
},
{
"path": "objects/storage/versioned_storage.rb",
"chars": 601,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Storage that adds "
},
{
"path": "objects/templates/github_tickets_body.haml",
"chars": 940,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\nThe puzzle `#{puzzle"
},
{
"path": "objects/templates/gitlab_tickets_body.haml",
"chars": 940,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\nThe puzzle `#{puzzle"
},
{
"path": "objects/templates/jira_tickets_body.haml",
"chars": 940,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\nThe puzzle `#{puzzle"
},
{
"path": "objects/tickets/commit_tickets.rb",
"chars": 1685,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Tickets that post "
},
{
"path": "objects/tickets/emailed_tickets.rb",
"chars": 1711,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Tickets that email"
},
{
"path": "objects/tickets/logged_tickets.rb",
"chars": 1927,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'cgi'\nrequire_"
},
{
"path": "objects/tickets/milestone_tickets.rb",
"chars": 1630,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Tickets that inher"
},
{
"path": "objects/tickets/sentry_tickets.rb",
"chars": 1442,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'mail'\nrequire"
},
{
"path": "objects/tickets/tagged_tickets.rb",
"chars": 1789,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Tagged tickets.\n#\n"
},
{
"path": "objects/tickets/tickets.rb",
"chars": 2821,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'haml'\nrequire"
},
{
"path": "objects/truncated.rb",
"chars": 469,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# Truncated text.\n#\n"
},
{
"path": "objects/user_error.rb",
"chars": 151,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\n#\n# User Error\n#\nclass"
},
{
"path": "objects/vcs/github.rb",
"chars": 3487,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'octokit'\nrequ"
},
{
"path": "objects/vcs/gitlab.rb",
"chars": 4243,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'gitlab'\nrequi"
},
{
"path": "objects/vcs/jira.rb",
"chars": 1965,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'jira-ruby'\nre"
},
{
"path": "renovate.json",
"chars": 107,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\n \"config:base\"\n ]\n}\n"
},
{
"path": "test/fake_github.rb",
"chars": 3204,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nclass FakeGithub\n att"
},
{
"path": "test/fake_gitlab.rb",
"chars": 1973,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nclass FakeGitlab\n att"
},
{
"path": "test/fake_log.rb",
"chars": 340,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nclass FakeLog\n attr_r"
},
{
"path": "test/fake_repo.rb",
"chars": 461,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/fake_storage.rb",
"chars": 440,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/fake_tickets.rb",
"chars": 413,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nclass FakeTickets\n at"
},
{
"path": "test/test_0pdd.rb",
"chars": 7982,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'rack/test'\nre"
},
{
"path": "test/test__helper.rb",
"chars": 911,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nENV['RACK_ENV'] = 'tes"
},
{
"path": "test/test_cached_storage.rb",
"chars": 679,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_commit_tickets.rb",
"chars": 1304,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'yaml'\nrequire"
},
{
"path": "test/test_credentials.rb",
"chars": 2834,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'mail'\nrequire"
},
{
"path": "test/test_diff.rb",
"chars": 6196,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_diff_complicated.rb",
"chars": 1783,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_git_repo.rb",
"chars": 6160,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'tmpdir'\nrequi"
},
{
"path": "test/test_github.rb",
"chars": 541,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_github_invitations.rb",
"chars": 1527,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_github_tickets.rb",
"chars": 5130,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_gitlab.rb",
"chars": 530,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_job.rb",
"chars": 796,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_job_commiterrors.rb",
"chars": 891,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_job_detached.rb",
"chars": 585,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_job_emailed.rb",
"chars": 902,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'veil'\nrequire"
},
{
"path": "test/test_log.rb",
"chars": 569,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_logged_storage.rb",
"chars": 749,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_logged_tickets.rb",
"chars": 1404,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_maybe_text.rb",
"chars": 797,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_milestone_tickets.rb",
"chars": 3575,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_once_storage.rb",
"chars": 979,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_puzzles.rb",
"chars": 2861,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_safe_storage.rb",
"chars": 1554,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_sentry_tickets.rb",
"chars": 823,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'mail'\nrequire"
},
{
"path": "test/test_svg.rb",
"chars": 2218,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_truncated.rb",
"chars": 1025,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire_relative 'test"
},
{
"path": "test/test_upgraded_storage.rb",
"chars": 1662,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test/test_versioned_storage.rb",
"chars": 698,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nrequire 'nokogiri'\nreq"
},
{
"path": "test-assets/puzzles/closes-one-puzzle.xml",
"chars": 1132,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "test-assets/puzzles/ignores-unknown-issues.xml",
"chars": 874,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "test-assets/puzzles/notify-unknown-open-issues.xml",
"chars": 425,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "test-assets/puzzles/simple.xml",
"chars": 2024,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "test-assets/puzzles/submits-old-puzzles.xml",
"chars": 2361,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "test-assets/puzzles/submits-ranked-puzzles.xml",
"chars": 1706,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "test-assets/puzzles/submits-three-tickets.xml",
"chars": 1680,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n * SPDX-L"
},
{
"path": "version.rb",
"chars": 123,
"preview": "# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n# SPDX-License-Identifier: MIT\n\nVERSION = 'BUILD'.free"
},
{
"path": "views/_footer.haml",
"chars": 121,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\n%p\n ='--'\n%p\n =ver"
},
{
"path": "views/_header.haml",
"chars": 375,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\n%p\n %a{href:'/'}\n "
},
{
"path": "views/error.haml",
"chars": 429,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\n%p\n %a{href:'/'}\n "
},
{
"path": "views/error_400.haml",
"chars": 142,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\n%p\n Request error\n%"
},
{
"path": "views/index.haml",
"chars": 1553,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\n%div.center\n %p\n "
},
{
"path": "views/item.haml",
"chars": 830,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\n= Haml::Engine.new(F"
},
{
"path": "views/layout.haml",
"chars": 1007,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\n!!! 5\n%html\n %head\n"
},
{
"path": "views/log.haml",
"chars": 1324,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\n= Haml::Engine.new(F"
},
{
"path": "views/not_found.haml",
"chars": 173,
"preview": "-# SPDX-FileCopyrightText: Copyright (c) 2016-2026 Yegor Bugayenko\n-# SPDX-License-Identifier: MIT\n\n%p\n This page is no"
}
]
About this extraction
This page contains the full source code of the yegor256/0pdd GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 152 files (211.3 KB), approximately 66.6k tokens, and a symbol index with 476 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.