Skip to content

Instantly share code, notes, and snippets.

@brasic
Created September 24, 2021 02:28
Show Gist options
  • Save brasic/63fa7eabac02b6f1b8157034b2b3c46a to your computer and use it in GitHub Desktop.
Save brasic/63fa7eabac02b6f1b8157034b2b3c46a to your computer and use it in GitHub Desktop.
delete_refs.rb
#!/usr/bin/env ruby
require "net/http"
require "json"
require "optparse"
# example usage:
# ./delete_refs.rb --uri https://api.github.com/graphql --file <reflist.txt> --nwo some/nwo
# Delete a list of refs using the `UpdateRefs` GraphQL API.
class DeleteRefs
def initialize(refs:, endpoint:, nwo:, token:)
@refs = refs
@uri = URI(endpoint)
@owner, @name = nwo.split("/", 2)
@token = token
if !@owner || !@name
raise ArgumentError, "nwo #{nwo.inspect} is invalid, needs slash"
end
end
MUTATION = <<-'GRAPHQL'
mutation($input: UpdateRefsInput!) {
updateRefs(input: $input) {
clientMutationId
}
}
GRAPHQL
GET_ID = <<-'GRAPHQL'
query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
}
}
GRAPHQL
NULL_OID = "0"*40
BATCH_SIZE = 200
def generate_deletion_payload(ref_chunk)
JSON.generate(
query: MUTATION,
variables: {
input: {
repositoryId: @repo_id,
refUpdates: ref_chunk.map { |ref_name|
{ name: ref_name, afterOid: NULL_OID }
}
}
}
)
end
def generate_get_id_payload
JSON.generate(
query: GET_ID,
variables: {
owner: @owner,
name: @name
}
)
end
def parse_response(response)
if response.code != "200"
raise "request failed with status #{response.code}:\n#{response.body}"
end
parsed = JSON.parse(response.body)
if parsed.key?("errors")
raise "error: #{parsed["errors"][0].values_at("type", "message")}"
end
parsed
end
def get_repo_id(http)
request = build_request(generate_get_id_payload)
response = parse_response(http.request(request))
response.dig("data", "repository", "id") or fail "bad data:#{parsed.inspect}"
end
def delete_refs(ref_chunk, http)
request = build_request(generate_deletion_payload(ref_chunk))
response = parse_response(http.request(request))
end
def call
count = 0
Net::HTTP.start(@uri.host, @uri.port, use_ssl: (@uri.scheme == 'https')) do |http|
@repo_id = get_repo_id(http)
@refs.each_slice(BATCH_SIZE) do |slice|
delete_refs(slice, http)
count += slice.size
print "refs deleted so far: #{count}\r"
end
end
puts "\ndone! total refs deleted: #{count}"
end
def build_request(payload)
request = Net::HTTP::Post.new @uri
request["Authorization"] = "bearer #{@token}"
request["Accept"] = "application/vnd.github.update-refs-preview"
request.body = payload
request
end
end
if $PROGRAM_NAME == __FILE__
def get_ref_list(path)
lines = File.readlines(path).map(&:chomp).uniq
if lines.any?{ |l| l.start_with?("refs/") }
puts "all refs must be qualified (start with `refs/`)"
exit 1
end
lines
end
options = {}
ARGV << "-h" if ARGV.empty?
parser = OptionParser.new do |parser|
parser.banner = "Usage: TOKEN=<token> #{$PROGRAM_NAME} [options]"
parser.on("--uri URI",
"Full URI of graphql endpoint, e.g. 'https://api.github.com/graphql'")
parser.on("-f", "--file FILE",
"Path to the file containing the list of refs to delete")
parser.on("--nwo NWO",
"Name With Owner of the repo to target, e.g. 'golang/go'")
parser.on("-h", "--help", "Show this message") do
puts parser
exit
end
end.parse!(into: options)
unless ENV.key?("TOKEN")
puts "Please set the TOKEN env var to a PAT with sufficient rights"
exit 1
end
DeleteRefs.new(
nwo: options.fetch(:nwo),
endpoint: options.fetch(:uri),
token: ENV.fetch("TOKEN"),
refs: get_ref_list(options.fetch(:file))
).call
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment