mirror of
https://github.com/wavetermdev/homebrew-cask.git
synced 2026-08-05 13:43:24 -07:00
brew-cask: move to using tap cmd directory.
This provides a few benefits: - faster `brew cask` execution times as another Ruby process is not needed. Cask can instead be loaded in-process with Homebrew. This will also make it easier to use some of Homebrew's core code and ease moving code from Cask into Homebrew core. - Users do not need to `brew upgrade` Cask any more: it's done automatically on any `brew update` or `git pull` of the Cask tap.
This commit is contained in:
@@ -1,118 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
#
|
||||
# bump_version
|
||||
#
|
||||
|
||||
###
|
||||
### dependencies
|
||||
###
|
||||
|
||||
require 'open3'
|
||||
require 'rubygems'
|
||||
|
||||
###
|
||||
### configurable constants
|
||||
###
|
||||
|
||||
VERSION_FILE = 'lib/hbc/version.rb'
|
||||
VERSION_PAT = %r[\d+\.\d+\.\d+]i
|
||||
VERSION_CONSTANT_NAME = 'HBC_VERSION'
|
||||
|
||||
###
|
||||
### methods
|
||||
###
|
||||
|
||||
def cd_to_project_root
|
||||
Dir.chdir File.dirname(File.expand_path(__FILE__))
|
||||
@git_root ||= Open3.popen3(*%w[
|
||||
git rev-parse --show-toplevel
|
||||
]) do |stdin, stdout, stderr|
|
||||
begin
|
||||
stdout.gets.chomp
|
||||
rescue
|
||||
end
|
||||
end
|
||||
Dir.chdir @git_root
|
||||
@git_root
|
||||
end
|
||||
|
||||
def git_diff
|
||||
Open3.popen3(*%w[
|
||||
git diff --no-color --
|
||||
],
|
||||
VERSION_FILE) do |stdin, stdout, stderr|
|
||||
stdout.each_line.map(&:chomp)
|
||||
end
|
||||
end
|
||||
|
||||
def file_contents_pat
|
||||
%r{\A(#{VERSION_CONSTANT_NAME}\s+=\s+)'(#{VERSION_PAT})'\s*\Z}s
|
||||
end
|
||||
|
||||
def current_file_contents
|
||||
@current_file_contents ||= File.read(VERSION_FILE)
|
||||
end
|
||||
|
||||
def current_file_version
|
||||
if file_contents_pat.match(current_file_contents)
|
||||
$2
|
||||
else
|
||||
raise "Could not parse file '#{VERSION_FILE}'"
|
||||
end
|
||||
end
|
||||
|
||||
def sanity_check
|
||||
unless %r{\A#{VERSION_PAT}\Z}.match(proposed_version)
|
||||
raise "Proposed version '#{proposed_version}' does not look like a semantic version string"
|
||||
end
|
||||
unless %r{\A#{VERSION_PAT}\Z}.match(current_file_version)
|
||||
raise "Current version '#{current_file_version}' does not look like a semantic version string"
|
||||
end
|
||||
unless Gem::Version.new(proposed_version) > Gem::Version.new(current_file_version)
|
||||
raise "Proposed version '#{proposed_version}' is not greater than current version #{current_file_version}"
|
||||
end
|
||||
end
|
||||
|
||||
def usage
|
||||
<<EOT
|
||||
bump_version <new-version>
|
||||
|
||||
Bump the version number in #{VERSION_FILE} to match the
|
||||
number given on the command line.
|
||||
|
||||
EOT
|
||||
end
|
||||
|
||||
def proposed_version
|
||||
@proposed_version ||= ARGV.first.sub(/^v/i, '')
|
||||
end
|
||||
|
||||
def rewrite_version
|
||||
new_file_contents = current_file_contents
|
||||
if new_file_contents.sub!(file_contents_pat, "\\1'#{proposed_version}'\n")
|
||||
File.open(VERSION_FILE, 'w') {|f| f.write(new_file_contents) }
|
||||
else
|
||||
raise "Could not parse file '#{VERSION_FILE}'"
|
||||
end
|
||||
end
|
||||
|
||||
###
|
||||
### main
|
||||
###
|
||||
|
||||
# process args
|
||||
if %r{\A-+h(?:elp)?}i.match(ARGV.first)
|
||||
puts usage
|
||||
exit
|
||||
elsif ARGV.length != 1 or ! %r{\Av?#{VERSION_PAT}\Z}.match(ARGV.first)
|
||||
puts usage
|
||||
exit 1
|
||||
end
|
||||
|
||||
# initialize
|
||||
cd_to_project_root
|
||||
|
||||
# dispatch
|
||||
sanity_check
|
||||
rewrite_version
|
||||
puts git_diff
|
||||
@@ -1,364 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
#
|
||||
# generate_changelog
|
||||
#
|
||||
|
||||
###
|
||||
### dependencies
|
||||
###
|
||||
|
||||
require 'open3'
|
||||
require 'set'
|
||||
|
||||
###
|
||||
### configurable constants
|
||||
###
|
||||
|
||||
PROJECT_URL = 'https://github.com/caskroom/homebrew-cask'
|
||||
|
||||
MAINTAINERS = %w[
|
||||
phinze
|
||||
fanquake
|
||||
vitorgalvao
|
||||
alebcay
|
||||
ndr-qef
|
||||
jawshooah
|
||||
sebroeder
|
||||
adityadalal924
|
||||
caskroom
|
||||
]
|
||||
|
||||
CODE_PATHS = %w[
|
||||
bin
|
||||
developer
|
||||
lib
|
||||
spec
|
||||
test
|
||||
brew-cask.rb
|
||||
Rakefile
|
||||
Gemfile
|
||||
Gemfile.lock
|
||||
.travis.yml
|
||||
.gitignore
|
||||
]
|
||||
|
||||
SHA_PAT = '[\da-f]{40}'
|
||||
|
||||
###
|
||||
### monkeypatching
|
||||
###
|
||||
|
||||
class Array
|
||||
def to_h
|
||||
Hash[*self.flatten]
|
||||
end
|
||||
end
|
||||
|
||||
###
|
||||
### git methods
|
||||
###
|
||||
|
||||
def end_object
|
||||
'HEAD'
|
||||
end
|
||||
|
||||
def matches_sha(sha)
|
||||
%r{\A#{SHA_PAT}\Z}.match(sha)
|
||||
end
|
||||
|
||||
def cd_to_project_root
|
||||
Dir.chdir File.dirname(File.expand_path(__FILE__))
|
||||
@git_root ||= Open3.popen3(*%w[
|
||||
git rev-parse --show-toplevel
|
||||
]) do |stdin, stdout, stderr|
|
||||
begin
|
||||
stdout.gets.chomp
|
||||
rescue
|
||||
end
|
||||
end
|
||||
Dir.chdir @git_root
|
||||
@git_root
|
||||
end
|
||||
|
||||
def warn_if_off_branch(wanted_branch='master')
|
||||
current_branch = Open3.popen3(*%w[
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
]) do |stdin, stdout, stderr|
|
||||
begin
|
||||
stdout.gets.chomp
|
||||
rescue
|
||||
end
|
||||
end
|
||||
unless current_branch == wanted_branch
|
||||
$stderr.puts "\nWARNING: you are running from branch '#{current_branch}', not '#{wanted_branch}'\n\n"
|
||||
end
|
||||
end
|
||||
|
||||
def last_release
|
||||
@last_release ||= Open3.popen3(
|
||||
'./developer/bin/get_release_tag'
|
||||
) do |stdin, stdout, stderr|
|
||||
begin
|
||||
stdout.gets.chomp
|
||||
rescue
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def next_release
|
||||
if @next_release.nil?
|
||||
if ENV.key?('NEW_RELEASE_TAG')
|
||||
@next_release = ENV['NEW_RELEASE_TAG']
|
||||
else
|
||||
@next_release = Open3.popen3(
|
||||
'./developer/bin/get_release_tag', '-next'
|
||||
) do |stdin, stdout, stderr|
|
||||
begin
|
||||
stdout.gets.chomp
|
||||
rescue
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
@next_release
|
||||
end
|
||||
end
|
||||
|
||||
def verify_git_object(object)
|
||||
sha = Open3.popen3(*%w[
|
||||
git rev-parse -q --verify
|
||||
],
|
||||
object, '--'
|
||||
) do |stdin, stdout, stderr|
|
||||
begin
|
||||
stdout.gets.chomp
|
||||
rescue
|
||||
end
|
||||
end
|
||||
raise "'#{object}' is not a git object" unless matches_sha sha
|
||||
sha
|
||||
end
|
||||
|
||||
# constrained to last_release..HEAD
|
||||
def all_shas
|
||||
@all_shas ||= Open3.popen3(*%w[
|
||||
git rev-list --topo-order
|
||||
],
|
||||
"#{last_release}..#{end_object}"
|
||||
) do |stdin, stdout, stderr|
|
||||
stdout.each_line.map(&:chomp)
|
||||
end
|
||||
end
|
||||
|
||||
# not constrained
|
||||
def tag_commits
|
||||
@tag_commits ||= Open3.popen3(*%w[
|
||||
git show-ref -s --tags --dereference
|
||||
]) do |stdin, stdout, stderr|
|
||||
stdout.each_line.collect do |line|
|
||||
line.chomp!
|
||||
if ! %r{\^\{\}\Z}.match(line)
|
||||
nil
|
||||
elsif %r{\A(#{SHA_PAT}) }.match(line)
|
||||
[$1, true]
|
||||
else
|
||||
raise "'#{line}' does not contain an SHA"
|
||||
end
|
||||
end.compact.to_h
|
||||
end
|
||||
end
|
||||
|
||||
# Collect merge commits separately because "git log" does not always
|
||||
# return related merge commits when using a path constraint. There
|
||||
# might be a clever way to do this in a single step already built into
|
||||
# git. In any case (in the opinion of the author) git's default
|
||||
# behavior is a bug.
|
||||
#
|
||||
# constrained to last_release..HEAD
|
||||
def merge_commits
|
||||
@merge_commits ||= Open3.popen3(*%w[
|
||||
git rev-list --pretty=oneline --topo-order --min-parents=2 --max-parents=2 --parents
|
||||
],
|
||||
"#{last_release}..#{end_object}"
|
||||
) do |stdin, stdout, stderr|
|
||||
stdout.each_line.collect do |line|
|
||||
line.chomp!
|
||||
# intentionally limited to the simple case of two parents
|
||||
if %r{\A(#{SHA_PAT}) (#{SHA_PAT}) (#{SHA_PAT}) (.*)}.match(line)
|
||||
[$1, {
|
||||
:trunk_parent => $2,
|
||||
:branch_parent => $3,
|
||||
:log => $4,
|
||||
}]
|
||||
else
|
||||
raise "could not parse '#{line}'"
|
||||
end
|
||||
end.compact.to_h
|
||||
end
|
||||
end
|
||||
|
||||
# constrained to last_release..HEAD
|
||||
# also constrained to CODE_PATHS
|
||||
def ordinary_code_commits
|
||||
@ordinary_code_commits ||= Open3.popen3(*%w[
|
||||
git rev-list --pretty=oneline --topo-order --max-parents=1 --parents
|
||||
],
|
||||
"#{last_release}..#{end_object}",
|
||||
'--', *CODE_PATHS
|
||||
) do |stdin, stdout, stderr|
|
||||
stdout.each_line.collect do |line|
|
||||
line.chomp!
|
||||
if %r{\A(#{SHA_PAT}) (#{SHA_PAT}) (.*)}.match(line)
|
||||
[$1, {
|
||||
:trunk_parent => $2,
|
||||
:log => $3,
|
||||
}]
|
||||
else
|
||||
raise "could not parse '#{line}'"
|
||||
end
|
||||
end.compact.to_h
|
||||
end
|
||||
end
|
||||
|
||||
###
|
||||
### report/analysis methods
|
||||
###
|
||||
|
||||
# todo: read the release date from the tag
|
||||
def header
|
||||
<<EOT
|
||||
## #{next_release.sub(/^v/,'')}
|
||||
|
||||
* __Casks__
|
||||
- N Casks added ...
|
||||
- N total Casks
|
||||
* __Features__
|
||||
- none
|
||||
* __Breaking Changes__
|
||||
- none
|
||||
* __Fixes__
|
||||
- none
|
||||
* __Internal Changes__
|
||||
- none
|
||||
* __Documentation__
|
||||
- N doc commits since ...
|
||||
* __Contributors__
|
||||
- N new contributors since ...
|
||||
- N total contributors
|
||||
* __Release Date__
|
||||
- YYYY-MM-DD HH:MM:SS UTC
|
||||
EOT
|
||||
end
|
||||
|
||||
def footer
|
||||
@footer ||= Set.new
|
||||
@footer.to_a.sort
|
||||
end
|
||||
|
||||
def add_to_footer(line)
|
||||
@footer ||= Set.new
|
||||
@footer.add line
|
||||
end
|
||||
|
||||
def seen(sha)
|
||||
@seen ||= {}
|
||||
if @seen[sha]
|
||||
return true
|
||||
else
|
||||
@seen[sha] = true
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def log_ordinary_commit(sha)
|
||||
# indent ordinary commits
|
||||
" - #{ordinary_code_commits[sha][:log]}"
|
||||
end
|
||||
|
||||
def read_pull_request(sha)
|
||||
branch_parent = merge_commits[sha][:branch_parent]
|
||||
if ordinary_code_commits[branch_parent] and
|
||||
%r{\AMerge pull request \#(\d+) from ([^\s/]+)}.match(merge_commits[sha][:log]) then
|
||||
{
|
||||
:num => $1,
|
||||
:gh_user => MAINTAINERS.include?($2) ? '' : $2
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def log_merge_commit(sha)
|
||||
branch_parent = merge_commits[sha][:branch_parent]
|
||||
pr = read_pull_request sha
|
||||
if pr then
|
||||
# munge a GitHub PR commit log entry into Markdown links
|
||||
# plus log content from the first parent commit
|
||||
log = "[##{pr[:num]}][]"
|
||||
log.concat " #{ordinary_code_commits[branch_parent][:log]}"
|
||||
add_to_footer "[##{pr[:num]}]: #{PROJECT_URL}/issues/#{pr[:num]}"
|
||||
seen branch_parent
|
||||
if pr[:gh_user].length > 0
|
||||
log.concat " <3 [@#{pr[:gh_user]}][]"
|
||||
add_to_footer "[@#{pr[:gh_user]}]: https://github.com/#{pr[:gh_user]}"
|
||||
end
|
||||
" - #{log}"
|
||||
elsif ordinary_code_commits[branch_parent]
|
||||
# non-PR merge, just pass the log msg unmodified
|
||||
" - #{merge_commits[sha][:log]}"
|
||||
else
|
||||
# drop this merge, it does not relate to CODE_PATHS
|
||||
end
|
||||
end
|
||||
|
||||
def changelog
|
||||
# follows topological order
|
||||
all_shas.collect do |sha|
|
||||
if tag_commits[sha]
|
||||
nil
|
||||
elsif seen sha
|
||||
nil
|
||||
elsif ordinary_code_commits[sha]
|
||||
log_ordinary_commit sha
|
||||
elsif merge_commits[sha]
|
||||
log_merge_commit sha
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
|
||||
###
|
||||
### main
|
||||
###
|
||||
|
||||
# process args
|
||||
if %r{\A-+h(?:elp)?}i.match(ARGV.first)
|
||||
puts <<EOT
|
||||
generate_changelog [ <release-tag> ]
|
||||
|
||||
Generate a rough-draft changelog in Markdown format for changes since
|
||||
<release-tag>, which defaults to the most recent release.
|
||||
|
||||
The output is only a draft. Changelog items still need to be edited,
|
||||
removed, and/or added.
|
||||
|
||||
All changelog items must also be moved to within one of the given
|
||||
category sections.
|
||||
|
||||
EOT
|
||||
exit
|
||||
end
|
||||
|
||||
if ARGV.length
|
||||
@last_release = ARGV.shift
|
||||
end
|
||||
|
||||
# initialize
|
||||
cd_to_project_root
|
||||
verify_git_object last_release
|
||||
warn_if_off_branch 'master'
|
||||
|
||||
# report
|
||||
puts header
|
||||
puts "\n"
|
||||
puts changelog
|
||||
puts "\n"
|
||||
puts footer
|
||||
puts "\n"
|
||||
@@ -50,6 +50,6 @@ if ! /usr/bin/which ronn >/dev/null 2>&1; then
|
||||
die "ERROR: The 'ronn' gem must be installed"
|
||||
fi
|
||||
|
||||
ronn --roff --pipe --organization='Homebrew-cask' --manual='brew-cask' doc/src/brew-cask.1.md > doc/man/brew-cask.1
|
||||
ronn --roff --pipe --organization='Homebrew-cask' --manual='brew-cask' doc/src/brew-cask.1.md > man/man1/brew-cask.1
|
||||
|
||||
#
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# get_release_tag
|
||||
#
|
||||
|
||||
###
|
||||
### settings
|
||||
###
|
||||
|
||||
set -e # exit on any uncaught error
|
||||
set +o histexpand # don't expand history expressions
|
||||
shopt -s nocasematch # case-insensitive regular expressions
|
||||
|
||||
###
|
||||
### global variables
|
||||
###
|
||||
|
||||
opt_next=''
|
||||
opt_latest=''
|
||||
opt_verbose=''
|
||||
|
||||
###
|
||||
### functions
|
||||
###
|
||||
|
||||
warn () {
|
||||
local message="$@"
|
||||
message="${message//\\t/$'\011'}"
|
||||
message="${message//\\n/$'\012'}"
|
||||
message="${message%"${message##*[![:space:]]}"}"
|
||||
printf "%s\n" "$message" 1>&2
|
||||
}
|
||||
|
||||
die () {
|
||||
warn "$@"
|
||||
exit 1
|
||||
}
|
||||
|
||||
cd_to_project_root () {
|
||||
local script_dir="$(/usr/bin/dirname "$0")"
|
||||
cd "$script_dir"
|
||||
local git_root="$(git rev-parse --show-toplevel)"
|
||||
if [[ -z "$git_root" ]]; then
|
||||
die "ERROR: Could not find git project root"
|
||||
fi
|
||||
cd "$git_root"
|
||||
}
|
||||
|
||||
verify_git_object_is_new () {
|
||||
if git rev-parse --verify "$1" >/dev/null 2>&1; then
|
||||
die "\nERROR: Proposed new tag: '$1' already exists as a commit object\n\n"
|
||||
fi
|
||||
}
|
||||
|
||||
sanity_check_parsed_version () {
|
||||
if [[ $# -ne 3 ]]; then
|
||||
die "ERROR: Could not parse version tag: wrong number of elements"
|
||||
fi
|
||||
if ! [[ $1 =~ ^v[0-9]+$ ]]; then
|
||||
die "ERROR: Could not parse version tag: does not start with v[0-9]"
|
||||
fi
|
||||
}
|
||||
|
||||
output_proposed_tag () {
|
||||
local new_tag="$1.$2.$3"
|
||||
sanity_check_parsed_version "$@"
|
||||
verify_git_object_is_new "$new_tag"
|
||||
if [[ -n "$opt_verbose" ]]; then
|
||||
printf "Latest tag\t%s\n" "$latest_tag"
|
||||
printf "Proposed new tag\t%s\n" "$new_tag"
|
||||
else
|
||||
printf "%s\n" "$new_tag"
|
||||
fi
|
||||
}
|
||||
|
||||
generate_next_major_tag () {
|
||||
local -a version_elts
|
||||
IFS='.' read -a version_elts <<< "$1"
|
||||
sanity_check_parsed_version "${version_elts[@]}"
|
||||
version_elts[0]="${version_elts[0]#v}" # mangle v<digit> into number
|
||||
(( version_elts[0] += 1 )) # increment version field
|
||||
version_elts[0]="v${version_elts[0]}" # restore text v<digit>
|
||||
version_elts[1]='0' # reset minor field
|
||||
version_elts[2]='0' # reset patch field
|
||||
output_proposed_tag "${version_elts[@]}"
|
||||
}
|
||||
|
||||
generate_next_minor_tag () {
|
||||
local -a version_elts
|
||||
IFS='.' read -a version_elts <<< "$1"
|
||||
sanity_check_parsed_version "${version_elts[@]}"
|
||||
(( version_elts[1] += 1 )) # increment minor field
|
||||
version_elts[2]='0' # reset patch field
|
||||
output_proposed_tag "${version_elts[@]}"
|
||||
}
|
||||
|
||||
generate_next_patch_tag () {
|
||||
local -a version_elts
|
||||
IFS='.' read -a version_elts <<< "$1"
|
||||
sanity_check_parsed_version "${version_elts[@]}"
|
||||
(( version_elts[2] += 1 )) # increment patch field
|
||||
output_proposed_tag "${version_elts[@]}"
|
||||
}
|
||||
|
||||
process_args () {
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
if [[ $arg =~ ^-+h(elp)?$ ]]; then
|
||||
printf "get_release_tag [ -latest | -next | -patch | -major | -verbose | -help ]
|
||||
|
||||
Retrieve or calculate a release tag.
|
||||
|
||||
Arguments:
|
||||
|
||||
-latest Show the most recent tag in the repository, which is typically
|
||||
the designation of the latest release. This is the default
|
||||
when invoked with no arguments.
|
||||
|
||||
-next Calculate the next minor-release tag bump and return it.
|
||||
|
||||
-patch Calculate the next patch-release tag bump and return it.
|
||||
Overrides -next if both are given.
|
||||
|
||||
-major Calculate the next major-release tag bump and return it.
|
||||
Overrides -next if both are given.
|
||||
|
||||
-verbose Output a table, possibly including both latest existing and
|
||||
proposed new tags.
|
||||
|
||||
See doc/releasing.md for more information.
|
||||
|
||||
"
|
||||
exit
|
||||
elif [[ $arg =~ ^-+v(erbose)?$ ]]; then
|
||||
opt_verbose='true'
|
||||
elif [[ $arg =~ ^-+next$ ]]; then
|
||||
if [[ -z "$opt_next" ]]; then
|
||||
opt_next='minor'
|
||||
fi
|
||||
elif [[ $arg =~ ^-+patch$ ]]; then
|
||||
if [[ "$opt_next" == 'major' ]]; then
|
||||
die "ERROR: -patch is incompatible with -major"
|
||||
fi
|
||||
opt_next='patch'
|
||||
elif [[ $arg =~ ^-+major$ ]]; then
|
||||
if [[ "$opt_next" == 'patch' ]]; then
|
||||
die "ERROR: -patch is incompatible with -major"
|
||||
fi
|
||||
opt_next='major'
|
||||
elif [[ $arg =~ ^-+latest$ ]]; then
|
||||
opt_latest='true'
|
||||
else
|
||||
die "ERROR: Unknown argument '$arg'"
|
||||
fi
|
||||
if [[ -n "$opt_latest" && -n "$opt_next" ]]; then
|
||||
die "ERROR: -latest is incompatible with -next/-patch/-major"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
###
|
||||
### main
|
||||
###
|
||||
|
||||
_get_release_tag () {
|
||||
cd_to_project_root
|
||||
local latest_tag="$(git describe --tags --abbrev=0 2>/dev/null)"
|
||||
if [[ -z "$latest_tag" ]]; then
|
||||
die "ERROR: No recent tag found"
|
||||
elif [[ -z "$opt_next" ]]; then
|
||||
if [[ -n "$opt_verbose" ]]; then
|
||||
printf "Latest tag\t"
|
||||
fi
|
||||
printf "%s\n" "$latest_tag"
|
||||
elif [[ "$opt_next" == 'major' ]]; then
|
||||
generate_next_major_tag "$latest_tag"
|
||||
elif [[ "$opt_next" == 'minor' ]]; then
|
||||
generate_next_minor_tag "$latest_tag"
|
||||
elif [[ "$opt_next" == 'patch' ]]; then
|
||||
generate_next_patch_tag "$latest_tag"
|
||||
else
|
||||
die "ERROR: Should not happen. Unknown argument?"
|
||||
fi
|
||||
}
|
||||
|
||||
process_args "${@}"
|
||||
|
||||
# dispatch main
|
||||
_get_release_tag
|
||||
|
||||
#
|
||||
Reference in New Issue
Block a user