diff --git a/src/GitForge.jl b/src/GitForge.jl index ac69f60..249eb55 100644 --- a/src/GitForge.jl +++ b/src/GitForge.jl @@ -35,5 +35,6 @@ include("api.jl") include(joinpath("forges", "GitHub", "GitHub.jl")) include(joinpath("forges", "GitLab", "GitLab.jl")) include(joinpath("forges", "Bitbucket", "Bitbucket.jl")) +include(joinpath("forges", "Codeberg", "Codeberg.jl")) end diff --git a/src/forges/Codeberg/Codeberg.jl b/src/forges/Codeberg/Codeberg.jl new file mode 100644 index 0000000..9bfaddf --- /dev/null +++ b/src/forges/Codeberg/Codeberg.jl @@ -0,0 +1,106 @@ +module Codeberg + +import ..GitForge: endpoint, into, postprocessor, @forge + +using ..GitForge +using ..GitForge: + @json, + AStr, + DoNothing, + DoSomething, + Endpoint, + Forge, + JSON, + OnRateLimit, + RateLimiter, + HEADERS, + ORL_THROW, + @not_implemented + +using Dates +using HTTP +using JSON3: JSON3 + +export CodebergAPI, NoToken, Token + +const DEFAULT_URL = "https://codeberg.org/api/v1" +const DEFAULT_DATEFORMAT = dateformat"yyyy-mm-ddTHH:MM:SS\Z" + +abstract type AbstractToken end + +""" + NoToken() + +Represents no authentication. +Only public data will be available. +""" +struct NoToken <: AbstractToken end + +""" + Token(token::$AStr) + +A personal access token for Codeberg/Forgejo/Gitea. +""" +struct Token <: AbstractToken + token::String +end + +auth_headers(::NoToken) = [] +auth_headers(t::Token) = ["Authorization" => "token $(t.token)"] + +""" + CodebergAPI(; + token::AbstractToken=NoToken(), + url::$AStr="$DEFAULT_URL", + has_rate_limits::Bool=false, + on_rate_limit::OnRateLimit=ORL_THROW, + ) + +Create a Codeberg/Forgejo/Gitea API client. + +## Keywords +- `token::AbstractToken=NoToken()`: Authorization token (or lack thereof). +- `url::$AStr="$DEFAULT_URL"`: Base URL of the target instance. +- `has_rate_limits::Bool=false`: Whether or not the server has rate limits. +- `on_rate_limit::OnRateLimit=ORL_THROW`: Behaviour on exceeded rate limits. +""" +struct CodebergAPI <: Forge + token::AbstractToken + url::String + hasrl::Bool + orl::OnRateLimit + rl::RateLimiter + + function CodebergAPI(; + token::AbstractToken=NoToken(), + url::AStr=DEFAULT_URL, + has_rate_limits::Bool=false, + on_rate_limit::OnRateLimit=ORL_THROW, + ) + return new(token, url, has_rate_limits, on_rate_limit, RateLimiter()) + end +end +@forge CodebergAPI + +GitForge.base_url(c::CodebergAPI) = c.url +GitForge.request_headers(c::CodebergAPI, ::Function) = [HEADERS; auth_headers(c.token)] +GitForge.postprocessor(::CodebergAPI, ::Function) = JSON() +GitForge.has_rate_limits(c::CodebergAPI, ::Function) = c.hasrl +GitForge.rate_limit_check(c::CodebergAPI, ::Function) = GitForge.rate_limit_check(c.rl) +GitForge.on_rate_limit(c::CodebergAPI, ::Function) = c.orl +GitForge.rate_limit_wait(c::CodebergAPI, ::Function) = GitForge.rate_limit_wait(c.rl) +GitForge.rate_limit_period(c::CodebergAPI, ::Function) = GitForge.rate_limit_period(c.rl) +GitForge.rate_limit_update!(c::CodebergAPI, ::Function, r::HTTP.Response) = + GitForge.rate_limit_update!(c.rl, r) + +include("users.jl") +include("repositories.jl") +include("pull_requests.jl") +include("commits.jl") +include("branches.jl") +include("tags.jl") +include("organizations.jl") + +ismemberorcollaborator(r::HTTP.Response) = r.status == 204 + +end diff --git a/src/forges/Codeberg/branches.jl b/src/forges/Codeberg/branches.jl new file mode 100644 index 0000000..dfddf46 --- /dev/null +++ b/src/forges/Codeberg/branches.jl @@ -0,0 +1,59 @@ +@json struct PayloadCommit + id::String + message::String + url::String + author::CommitUser + committer::CommitUser + timestamp::DateTime +end + +@json struct BranchProtection + branch_name::String + enable_push::Bool + enable_push_whitelist::Bool + push_whitelist_usernames::Vector{String} + push_whitelist_teams::Vector{String} + push_whitelist_deploy_keys::Bool + enable_merge_whitelist::Bool + merge_whitelist_usernames::Vector{String} + merge_whitelist_teams::Vector{String} + enable_status_check::Bool + status_check_contexts::Vector{String} + required_approvals::Int + enable_approvals_whitelist::Bool + approvals_whitelist_usernames::Vector{String} + approvals_whitelist_teams::Vector{String} + block_on_rejected_reviews::Bool + block_on_official_review_requests::Bool + block_on_outdated_branch::Bool + dismiss_stale_approvals::Bool + require_signed_commits::Bool + protected_file_patterns::String + unprotected_file_patterns::String + created_at::DateTime + updated_at::DateTime +end + +@json struct Branch + name::String + commit::PayloadCommit + protected::Bool + required_approvals::Int + enable_status_check::Bool + status_check_contexts::Vector{String} + user_can_push::Bool + user_can_merge::Bool + effective_branch_protection_name::String +end + +endpoint(::CodebergAPI, ::typeof(get_branch), owner::AStr, repo::AStr, branch::AStr) = + Endpoint(:GET, "/repos/$owner/$repo/branches/$branch") +into(::CodebergAPI, ::typeof(get_branch)) = Branch + +endpoint(::CodebergAPI, ::typeof(get_branches), owner::AStr, repo::AStr) = + Endpoint(:GET, "/repos/$owner/$repo/branches") +into(::CodebergAPI, ::typeof(get_branches)) = Vector{Branch} + +endpoint(::CodebergAPI, ::typeof(delete_branch), owner::AStr, repo::AStr, branch::AStr) = + Endpoint(:DELETE, "/repos/$owner/$repo/branches/$branch") +postprocessor(::CodebergAPI, ::typeof(delete_branch)) = DoNothing() diff --git a/src/forges/Codeberg/commits.jl b/src/forges/Codeberg/commits.jl new file mode 100644 index 0000000..393c93c --- /dev/null +++ b/src/forges/Codeberg/commits.jl @@ -0,0 +1,41 @@ +@json struct CommitUser + name::String + email::String + date::DateTime +end + +@json struct RepoCommit + url::String + author::CommitUser + committer::CommitUser + message::String +end + +@json struct CommitAffectedFiles + filename::String + status::String +end + +@json struct CommitStats + total::Int + additions::Int + deletions::Int +end + +@json struct Commit + url::String + sha::String + html_url::String + commit::RepoCommit + author::User + committer::User + parents::Vector{Commit} + files::Vector{CommitAffectedFiles} + stats::CommitStats + created::DateTime +end + +endpoint(::CodebergAPI, ::typeof(get_commit), owner::AStr, repo::AStr, ref::AStr) = + Endpoint(:GET, "/repos/$owner/$repo/git/commits/$ref") +@not_implemented(::CodebergAPI, ::typeof(get_commit), ::Integer, ::String) +into(::CodebergAPI, ::typeof(get_commit)) = Commit diff --git a/src/forges/Codeberg/organizations.jl b/src/forges/Codeberg/organizations.jl new file mode 100644 index 0000000..bc86220 --- /dev/null +++ b/src/forges/Codeberg/organizations.jl @@ -0,0 +1,39 @@ +@json struct Organization + id::Int + name::String + full_name::String + email::String + avatar_url::String + description::String + website::String + location::String + visibility::String + repo_admin_change_team_access::Bool + username::String +end + +endpoint(::CodebergAPI, ::typeof(is_member), org::AStr, user::AStr) = + Endpoint(:GET, "/orgs/$org/members/$user"; allow_404=true) +@not_implemented(::CodebergAPI, ::typeof(is_member), ::String, ::Integer) +postprocessor(::CodebergAPI, ::typeof(is_member)) = DoSomething(ismemberorcollaborator) +into(::CodebergAPI, ::typeof(is_member)) = Bool + +@not_implemented(::CodebergAPI, ::typeof(groups)) + +@not_implemented(::CodebergAPI, ::typeof(list_pull_request_comments), ::String, ::String, ::Integer) +@not_implemented(::CodebergAPI, ::typeof(list_pull_request_comments), ::Integer, ::Integer) + +@not_implemented(::CodebergAPI, ::typeof(get_pull_request_comment), ::String, ::String, ::Integer) +@not_implemented(::CodebergAPI, ::typeof(get_pull_request_comment), ::Integer, ::Integer, ::Integer) + +@not_implemented(::CodebergAPI, ::typeof(create_pull_request_comment), ::String, ::String, ::Integer) +@not_implemented(::CodebergAPI, ::typeof(create_pull_request_comment), ::Integer, ::Integer) + +@not_implemented(::CodebergAPI, ::typeof(update_pull_request_comment), ::String, ::String, ::Integer) +@not_implemented(::CodebergAPI, ::typeof(update_pull_request_comment), ::Integer, ::Integer, ::Integer) + +@not_implemented(::CodebergAPI, ::typeof(delete_pull_request_comment), ::String, ::String, ::Integer) +@not_implemented(::CodebergAPI, ::typeof(delete_pull_request_comment), ::Integer, ::Integer, ::Integer) + +@not_implemented(::CodebergAPI, ::typeof(list_pipeline_schedules), ::Integer) +@not_implemented(::CodebergAPI, ::typeof(list_pipeline_schedules), ::String, ::String) diff --git a/src/forges/Codeberg/pull_requests.jl b/src/forges/Codeberg/pull_requests.jl new file mode 100644 index 0000000..e03794b --- /dev/null +++ b/src/forges/Codeberg/pull_requests.jl @@ -0,0 +1,87 @@ +@json struct Label + id::Int + name::String + exclusive::Bool + is_archived::Bool + color::String + description::String + url::String +end + +@json struct Milestone + id::Int + title::String + description::String + state::String + open_issues::Int + closed_issues::Int + created_at::DateTime + updated_at::DateTime + closed_at::DateTime + due_on::DateTime +end + +@json struct PRBranchInfo + label::String + ref::String + sha::String + repo_id::Int + repo::Repo +end + +@json struct PullRequest + id::Int + url::String + number::Int + user::User + title::String + body::String + labels::Vector{Label} + milestone::Milestone + assignee::User + assignees::Vector{User} + requested_reviewers::Vector{User} + state::String + is_locked::Bool + comments::Int + html_url::String + diff_url::String + patch_url::String + mergeable::Bool + merged::Bool + merged_at::DateTime + merge_commit_sha::String + merged_by::User + allow_maintainer_edit::Bool + base::PRBranchInfo + head::PRBranchInfo + merge_base::String + due_date::DateTime + created_at::DateTime + updated_at::DateTime + closed_at::DateTime + pin_order::Int +end + +endpoint(::CodebergAPI, ::typeof(get_pull_requests), owner::AStr, repo::AStr) = + Endpoint(:GET, "/repos/$owner/$repo/pulls") +@not_implemented(::CodebergAPI, ::typeof(get_pull_requests), ::Integer) +into(::CodebergAPI, ::typeof(get_pull_requests)) = Vector{PullRequest} + +endpoint(::CodebergAPI, ::typeof(get_pull_request), owner::AStr, repo::AStr, number::Integer) = + Endpoint(:GET, "/repos/$owner/$repo/pulls/$number") +@not_implemented(::CodebergAPI, ::typeof(get_pull_request), ::Integer, ::Integer) +into(::CodebergAPI, ::typeof(get_pull_request)) = PullRequest + +endpoint(::CodebergAPI, ::typeof(create_pull_request), owner::AStr, repo::AStr) = + Endpoint(:POST, "/repos/$owner/$repo/pulls") +@not_implemented(::CodebergAPI, ::typeof(create_pull_request), ::Integer) +into(::CodebergAPI, ::typeof(create_pull_request)) = PullRequest + +endpoint(::CodebergAPI, ::typeof(update_pull_request), owner::AStr, repo::AStr, number::Integer) = + Endpoint(:PATCH, "/repos/$owner/$repo/pulls/$number") +@not_implemented(::CodebergAPI, ::typeof(update_pull_request), ::Integer, ::Integer) +into(::CodebergAPI, ::typeof(update_pull_request)) = PullRequest + +@not_implemented(::CodebergAPI, ::typeof(subscribe_to_pull_request), ::Integer, ::Integer) +@not_implemented(::CodebergAPI, ::typeof(unsubscribe_from_pull_request), ::Integer, ::Integer) diff --git a/src/forges/Codeberg/repositories.jl b/src/forges/Codeberg/repositories.jl new file mode 100644 index 0000000..1b47d8e --- /dev/null +++ b/src/forges/Codeberg/repositories.jl @@ -0,0 +1,132 @@ +@json struct Permission + admin::Bool + push::Bool + pull::Bool +end + +@json struct InternalTracker + enable_time_tracker::Bool + allow_only_contributors_to_track_time::Bool + enable_issue_dependencies::Bool +end + +@json struct ExternalTracker + external_tracker_url::String + external_tracker_format::String + external_tracker_style::String + external_tracker_regexp_pattern::String +end + +@json struct ExternalWiki + external_wiki_url::String +end + +@json struct Repo + id::Int + owner::User + name::String + full_name::String + description::String + empty::Bool + private::Bool + fork::Bool + template::Bool + parent::Repo + mirror::Bool + size::Int + language::String + languages_url::String + html_url::String + url::String + ssh_url::String + clone_url::String + original_url::String + website::String + stars_count::Int + forks_count::Int + watchers_count::Int + open_issues_count::Int + open_pr_counter::Int + release_counter::Int + default_branch::String + archived::Bool + created_at::DateTime + updated_at::DateTime + archived_at::DateTime + permissions::Permission + has_issues::Bool + internal_tracker::InternalTracker + external_tracker::ExternalTracker + has_wiki::Bool + external_wiki::ExternalWiki + has_pull_requests::Bool + has_projects::Bool + has_releases::Bool + has_packages::Bool + has_actions::Bool + ignore_whitespace_conflicts::Bool + allow_merge_commits::Bool + allow_rebase::Bool + allow_rebase_explicit::Bool + allow_squash_merge::Bool + allow_rebase_update::Bool + default_delete_branch_after_merge::Bool + default_merge_style::String + default_allow_maintainer_edit::Bool + avatar_url::String + internal::Bool + mirror_interval::String + mirror_updated::DateTime + repo_transfer::NamedTuple +end + +@json struct FileContentsLinks + self::String + git::String + html::String +end + +@json struct FileContents + type::String + encoding::String + size::Int + name::String + path::String + content::String + sha::String + url::String + html_url::String + git_url::String + download_url::String + submodule_git_url::String + _links => links::FileContentsLinks +end + +endpoint(::CodebergAPI, ::typeof(get_user_repos)) = Endpoint(:GET, "/user/repos") +endpoint(::CodebergAPI, ::typeof(get_user_repos), name::AStr) = Endpoint(:GET, "/users/$name/repos") +@not_implemented(::CodebergAPI, ::typeof(get_user_repos), ::Integer) +into(::CodebergAPI, ::typeof(get_user_repos)) = Vector{Repo} + +endpoint(::CodebergAPI, ::typeof(get_repo), owner::AStr, repo::AStr) = + Endpoint(:GET, "/repos/$owner/$repo") +@not_implemented(::CodebergAPI, ::typeof(get_repo), ::String) +@not_implemented(::CodebergAPI, ::typeof(get_repo), ::Integer) +@not_implemented(::CodebergAPI, ::typeof(get_repo), ::String, ::String, ::String) +into(::CodebergAPI, ::typeof(get_repo)) = Repo + +endpoint(::CodebergAPI, ::typeof(create_repo)) = Endpoint(:POST, "/user/repos") +endpoint(::CodebergAPI, ::typeof(create_repo), org::AStr) = Endpoint(:POST, "/orgs/$org/repos") +@not_implemented(::CodebergAPI, ::typeof(create_repo), ::Integer) +@not_implemented(::CodebergAPI, ::typeof(create_repo), ::String, ::String) +into(::CodebergAPI, ::typeof(create_repo)) = Repo + +endpoint(::CodebergAPI, ::typeof(is_collaborator), owner::AStr, repo::AStr, user::AStr) = + Endpoint(:GET, "/repos/$owner/$repo/collaborators/$user"; allow_404=true) +@not_implemented(::CodebergAPI, ::typeof(is_collaborator), ::String, ::String, ::Integer) +postprocessor(::CodebergAPI, ::typeof(is_collaborator)) = DoSomething(ismemberorcollaborator) +into(::CodebergAPI, ::typeof(is_collaborator)) = Bool + +endpoint(::CodebergAPI, ::typeof(get_file_contents), owner::AStr, repo::AStr, path::AStr) = + Endpoint(:GET, "/repos/$owner/$repo/contents/$path") +@not_implemented(::CodebergAPI, ::typeof(get_file_contents), ::Integer, ::String) +into(::CodebergAPI, ::typeof(get_file_contents)) = FileContents diff --git a/src/forges/Codeberg/tags.jl b/src/forges/Codeberg/tags.jl new file mode 100644 index 0000000..3eb2e9d --- /dev/null +++ b/src/forges/Codeberg/tags.jl @@ -0,0 +1,13 @@ +@json struct Tag + id::String + name::String + message::String + commit::PayloadCommit + zipball_url::String + tarball_url::String +end + +endpoint(::CodebergAPI, ::typeof(get_tags), owner::AStr, repo::AStr) = + Endpoint(:GET, "/repos/$owner/$repo/tags") +@not_implemented(::CodebergAPI, ::typeof(get_tags), ::Integer) +into(::CodebergAPI, ::typeof(get_tags)) = Vector{Tag} diff --git a/src/forges/Codeberg/users.jl b/src/forges/Codeberg/users.jl new file mode 100644 index 0000000..19f87b3 --- /dev/null +++ b/src/forges/Codeberg/users.jl @@ -0,0 +1,39 @@ +@json struct User + id::Int + login::String + login_name::String + full_name::String + email::String + avatar_url::String + html_url::String + language::String + is_admin::Bool + last_login::DateTime + created::DateTime + restricted::Bool + active::Bool + prohibit_login::Bool + location::String + website::String + description::String + visibility::String + followers_count::Int + following_count::Int + starred_repos_count::Int + username::String +end + +endpoint(::CodebergAPI, ::typeof(get_user)) = Endpoint(:GET, "/user") +endpoint(::CodebergAPI, ::typeof(get_user), name::AStr) = Endpoint(:GET, "/users/$name") +@not_implemented(::CodebergAPI, ::typeof(get_user), ::Int64) +into(::CodebergAPI, ::typeof(get_user)) = User + +endpoint(::CodebergAPI, ::typeof(get_users)) = Endpoint(:GET, "/admin/users") +into(::CodebergAPI, ::typeof(get_users)) = Vector{User} + +@not_implemented(::CodebergAPI, ::typeof(update_user)) +@not_implemented(::CodebergAPI, ::typeof(update_user), ::Integer) + +@not_implemented(::CodebergAPI, ::typeof(create_user)) + +@not_implemented(::CodebergAPI, ::typeof(delete_user), ::Integer) diff --git a/test/runtests.jl b/test/runtests.jl index f6007a4..0ea6471 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,15 +1,17 @@ using GitForge using UUIDs using GitForge: AStr, Endpoint, ForgeAPINotImplemented, ForgeAPIError -using GitForge: GitHub, GitLab, Bitbucket +using GitForge: GitHub, GitLab, Bitbucket, Codeberg using GitForge.GitHub: Token, GitHubAPI using GitForge.GitLab: GitLabAPI, OAuth2Token using GitForge.Bitbucket: BitbucketAPI +using GitForge.Codeberg: CodebergAPI, Token as CodebergToken using HTTP, JSON3, Logging using Test: @test, @testset, TestLogger, @test_throws const GH_TOKEN = Token("secret token") const GL_TOKEN = OAuth2Token("secret token") +const CB_TOKEN = CodebergToken("secret token") const USER = "a user" const WORKSPACE = "a workspace" const GF = GitForge @@ -118,6 +120,7 @@ mock_id(::Type{UUID}) = UUID(0) user_id_type(::GitHubAPI) = Integer user_id_type(::GitLabAPI) = Integer user_id_type(::BitbucketAPI) = UUID +user_id_type(::CodebergAPI) = Integer function test_api(api; auth = false) unimplemented = 0 @@ -176,6 +179,13 @@ exclude(_api, _func, _args...) = false @test_throws ForgeAPINotImplemented GF.endpoint(api, get_repo, "owner") end +@testset "Codeberg.jl" begin + api = CodebergAPI(; token = CB_TOKEN) + @test api.token == CB_TOKEN + @test api.url == Codeberg.DEFAULT_URL + test_api(api, auth = true) +end + @testset "GitHub.jl" begin api = GitHubAPI(; token = GH_TOKEN) @test api.token == GH_TOKEN @@ -197,6 +207,8 @@ const ENCODING_SAMPLES = [ GitLab.Project => "{\"id\":34333070,\"description\":\"\",\"name\":\"ExampleBranchSubdir.jl\",\"name_with_namespace\":\"Bill Burdick / ExampleBranchSubdir.jl\",\"path\":\"ExampleBranchSubdir.jl\",\"path_with_namespace\":\"zot1/ExampleBranchSubdir.jl\",\"created_at\":\"2022-03-09T07:30:26.467Z\",\"default_branch\":\"master\",\"tag_list\":[],\"topics\":[],\"ssh_url_to_repo\":\"git@gitlab.com:zot1/ExampleBranchSubdir.jl.git\",\"http_url_to_repo\":\"https://gitlab.com/zot1/ExampleBranchSubdir.jl.git\",\"web_url\":\"https://gitlab.com/zot1/ExampleBranchSubdir.jl\",\"readme_url\":null,\"avatar_url\":null,\"forks_count\":1,\"star_count\":0,\"last_activity_at\":\"2022-03-29T21:40:37.298Z\",\"namespace\":{\"id\":3495260,\"name\":\"Bill Burdick\",\"path\":\"zot1\",\"kind\":\"user\",\"full_path\":\"zot1\",\"parent_id\":null,\"avatar_url\":\"https://secure.gravatar.com/avatar/5ad6635635270fd81ab229ece9b2d2a0?s=80\\u0026d=identicon\",\"web_url\":\"https://gitlab.com/zot1\"},\"container_registry_image_prefix\":\"registry.gitlab.com/zot1/examplebranchsubdir.jl\",\"_links\":{\"self\":\"https://gitlab.com/api/v4/projects/34333070\",\"issues\":\"https://gitlab.com/api/v4/projects/34333070/issues\",\"merge_requests\":\"https://gitlab.com/api/v4/projects/34333070/merge_requests\",\"repo_branches\":\"https://gitlab.com/api/v4/projects/34333070/repository/branches\",\"labels\":\"https://gitlab.com/api/v4/projects/34333070/labels\",\"events\":\"https://gitlab.com/api/v4/projects/34333070/events\",\"members\":\"https://gitlab.com/api/v4/projects/34333070/members\",\"cluster_agents\":\"https://gitlab.com/api/v4/projects/34333070/cluster_agents\"},\"packages_enabled\":true,\"empty_repo\":false,\"archived\":false,\"visibility\":\"public\",\"owner\":{\"id\":2742710,\"username\":\"zot1\",\"name\":\"Bill Burdick\",\"state\":\"active\",\"avatar_url\":\"https://secure.gravatar.com/avatar/5ad6635635270fd81ab229ece9b2d2a0?s=80\\u0026d=identicon\",\"web_url\":\"https://gitlab.com/zot1\"},\"resolve_outdated_diff_discussions\":false,\"container_expiration_policy\":{\"cadence\":\"1d\",\"enabled\":false,\"keep_n\":10,\"older_than\":\"90d\",\"name_regex\":\".*\",\"name_regex_keep\":null,\"next_run_at\":\"2022-03-10T07:30:26.514Z\"},\"issues_enabled\":true,\"merge_requests_enabled\":true,\"wiki_enabled\":true,\"jobs_enabled\":true,\"snippets_enabled\":true,\"container_registry_enabled\":true,\"service_desk_enabled\":true,\"service_desk_address\":\"contact-project+zot1-examplebranchsubdir-jl-34333070-issue-@incoming.gitlab.com\",\"can_create_merge_request_in\":true,\"issues_access_level\":\"enabled\",\"repository_access_level\":\"enabled\",\"merge_requests_access_level\":\"enabled\",\"forking_access_level\":\"enabled\",\"wiki_access_level\":\"enabled\",\"builds_access_level\":\"enabled\",\"snippets_access_level\":\"enabled\",\"pages_access_level\":\"enabled\",\"operations_access_level\":\"enabled\",\"analytics_access_level\":\"enabled\",\"container_registry_access_level\":\"enabled\",\"security_and_compliance_access_level\":\"private\",\"emails_disabled\":null,\"shared_runners_enabled\":true,\"lfs_enabled\":true,\"creator_id\":2742710,\"import_url\":\"https://bitbucket.org/zot-julia/examplebranchsubdir.jl.git\",\"import_type\":\"bitbucket\",\"import_status\":\"finished\",\"import_error\":null,\"open_issues_count\":0,\"runners_token\":\"GR1348941BXpcpX3ysWGvAD3ZnN1e\",\"ci_default_git_depth\":20,\"ci_forward_deployment_enabled\":true,\"ci_job_token_scope_enabled\":false,\"ci_separated_caches\":true,\"ci_opt_in_jwt\":false,\"ci_allow_fork_pipelines_to_run_in_parent_project\":true,\"public_jobs\":true,\"build_git_strategy\":\"fetch\",\"build_timeout\":3600,\"auto_cancel_pending_pipelines\":\"enabled\",\"ci_config_path\":\"\",\"shared_with_groups\":[],\"only_allow_merge_if_pipeline_succeeds\":false,\"allow_merge_on_skipped_pipeline\":null,\"restrict_user_defined_variables\":false,\"request_access_enabled\":true,\"only_allow_merge_if_all_discussions_are_resolved\":false,\"remove_source_branch_after_merge\":true,\"printing_merge_request_link_enabled\":true,\"merge_method\":\"merge\",\"squash_option\":\"default_off\",\"enforce_auth_checks_on_uploads\":true,\"suggestion_commit_message\":null,\"merge_commit_template\":null,\"squash_commit_template\":null,\"auto_devops_enabled\":false,\"auto_devops_deploy_strategy\":\"continuous\",\"autoclose_referenced_issues\":true,\"keep_latest_artifact\":true,\"runner_token_expiration_interval\":null,\"external_authorization_classification_label\":\"\",\"requirements_enabled\":false,\"requirements_access_level\":\"enabled\",\"security_and_compliance_enabled\":true,\"compliance_frameworks\":[],\"permissions\":{\"project_access\":{\"access_level\":50,\"notification_level\":3},\"group_access\":null}}", Bitbucket.Repo => "{\"type\": \"repository\", \"full_name\": \"wrburdick/example.jl\", \"links\": {\"self\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl\"}, \"html\": {\"href\": \"https://bitbucket.org/wrburdick/example.jl\"}, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Bbb41ce36-a601-40ab-988c-3b4a37598e8f%7D?ts=default\"}, \"pullrequests\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/pullrequests\"}, \"commits\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/commits\"}, \"forks\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/forks\"}, \"watchers\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/watchers\"}, \"branches\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/refs/branches\"}, \"tags\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/refs/tags\"}, \"downloads\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/downloads\"}, \"source\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/src\"}, \"clone\": [{\"name\": \"https\", \"href\": \"https://wrburdick@bitbucket.org/wrburdick/example.jl.git\"}, {\"name\": \"ssh\", \"href\": \"git@bitbucket.org:wrburdick/example.jl.git\"}], \"hooks\": {\"href\": \"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/hooks\"}}, \"name\": \"Example.jl\", \"slug\": \"example.jl\", \"description\": \"\", \"scm\": \"git\", \"website\": null, \"owner\": {\"display_name\": \"wrburdick\", \"links\": {\"self\": {\"href\": \"https://api.bitbucket.org/2.0/users/%7Bbaa4b755-09fc-44d1-b6c6-894cdd9bbe32%7D\"}, \"avatar\": {\"href\": \"https://secure.gravatar.com/avatar/5ad6635635270fd81ab229ece9b2d2a0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FW-5.png\"}, \"html\": {\"href\": \"https://bitbucket.org/%7Bbaa4b755-09fc-44d1-b6c6-894cdd9bbe32%7D/\"}}, \"type\": \"user\", \"uuid\": \"{baa4b755-09fc-44d1-b6c6-894cdd9bbe32}\", \"account_id\": \"5ac0edc1b7687c3f7e5e8a57\", \"nickname\": \"wrburdick\"}, \"workspace\": {\"type\": \"workspace\", \"uuid\": \"{baa4b755-09fc-44d1-b6c6-894cdd9bbe32}\", \"name\": \"wrburdick\", \"slug\": \"wrburdick\", \"links\": {\"avatar\": {\"href\": \"https://bitbucket.org/workspaces/wrburdick/avatar/?ts=1543700346\"}, \"html\": {\"href\": \"https://bitbucket.org/wrburdick/\"}, \"self\": {\"href\": \"https://api.bitbucket.org/2.0/workspaces/wrburdick\"}}}, \"is_private\": false, \"project\": {\"type\": \"project\", \"key\": \"JUL3\", \"uuid\": \"{4afe0de1-1b45-4075-9cb9-378551b1299a}\", \"name\": \"Julia\", \"links\": {\"self\": {\"href\": \"https://api.bitbucket.org/2.0/workspaces/wrburdick/projects/JUL3\"}, \"html\": {\"href\": \"https://bitbucket.org/wrburdick/workspace/projects/JUL3\"}, \"avatar\": {\"href\": \"https://bitbucket.org/account/user/wrburdick/projects/JUL3/avatar/32?ts=1646463647\"}}}, \"fork_policy\": \"allow_forks\", \"created_on\": \"2022-01-17T16:21:42.416498+00:00\", \"updated_on\": \"2022-01-17T17:10:27.581218+00:00\", \"size\": 309449, \"language\": \"\", \"has_issues\": false, \"has_wiki\": false, \"uuid\": \"{bb41ce36-a601-40ab-988c-3b4a37598e8f}\", \"mainbranch\": {\"name\": \"master\", \"type\": \"branch\"}, \"override_settings\": {\"default_merge_strategy\": false, \"branching_model\": false}}", Bitbucket.Commit => "{\"participants\":[],\"parents\":[{\"links\":{\"self\":{\"href\":\"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/commit/d29ad46e7f17df304388024a2041bde4fcbbc819\"},\"html\":{\"href\":\"https://bitbucket.org/wrburdick/example.jl/commits/d29ad46e7f17df304388024a2041bde4fcbbc819\"}},\"hash\":\"d29ad46e7f17df304388024a2041bde4fcbbc819\",\"type\":\"commit\"}],\"author\":{\"raw\":\"Dilum Aluthge \",\"type\":\"author\",\"user\":{\"links\":{\"self\":{\"href\":\"https://api.bitbucket.org/2.0/users/%7B7dc90380-f131-43f7-b7c1-5aad8f0537c6%7D\"},\"html\":{\"href\":\"https://bitbucket.org/%7B7dc90380-f131-43f7-b7c1-5aad8f0537c6%7D/\"},\"avatar\":{\"href\":\"https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:985818f4-2fb5-4d9e-a7b2-1125d0013f8a/1fd6ad00-0bba-426e-b522-b7f0f7c4590f/128\"}},\"account_id\":\"557058:985818f4-2fb5-4d9e-a7b2-1125d0013f8a\",\"uuid\":\"{7dc90380-f131-43f7-b7c1-5aad8f0537c6}\",\"display_name\":\"Dilum Aluthge\",\"nickname\":\"dilumaluthge\",\"type\":\"user\"}},\"repository\":{\"name\":\"Example.jl\",\"links\":{\"self\":{\"href\":\"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl\"},\"html\":{\"href\":\"https://bitbucket.org/wrburdick/example.jl\"},\"avatar\":{\"href\":\"https://bytebucket.org/ravatar/%7Bbb41ce36-a601-40ab-988c-3b4a37598e8f%7D?ts=default\"}},\"uuid\":\"{bb41ce36-a601-40ab-988c-3b4a37598e8f}\",\"full_name\":\"wrburdick/example.jl\",\"type\":\"repository\"},\"links\":{\"statuses\":{\"href\":\"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/commit/9f288cc4864dbfc2c82338caac0b1eb1fc7ff87f/statuses\"},\"patch\":{\"href\":\"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/patch/9f288cc4864dbfc2c82338caac0b1eb1fc7ff87f\"},\"self\":{\"href\":\"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/commit/9f288cc4864dbfc2c82338caac0b1eb1fc7ff87f\"},\"approve\":{\"href\":\"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/commit/9f288cc4864dbfc2c82338caac0b1eb1fc7ff87f/approve\"},\"html\":{\"href\":\"https://bitbucket.org/wrburdick/example.jl/commits/9f288cc4864dbfc2c82338caac0b1eb1fc7ff87f\"},\"diff\":{\"href\":\"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/diff/9f288cc4864dbfc2c82338caac0b1eb1fc7ff87f\"},\"comments\":{\"href\":\"https://api.bitbucket.org/2.0/repositories/wrburdick/example.jl/commit/9f288cc4864dbfc2c82338caac0b1eb1fc7ff87f/comments\"}},\"type\":\"commit\",\"message\":\"Fix whitespace in `Project.toml` to match the way that Pkg prints it out (#61)\\n\\n\",\"rendered\":{\"message\":{\"raw\":\"Fix whitespace in `Project.toml` to match the way that Pkg prints it out (#61)\\n\\n\",\"html\":\"

Fix whitespace in Project.toml to match the way that Pkg prints it out (#61)

\",\"markup\":\"markdown\",\"type\":\"rendered\"}},\"summary\":{\"raw\":\"Fix whitespace in `Project.toml` to match the way that Pkg prints it out (#61)\\n\\n\",\"html\":\"

Fix whitespace in Project.toml to match the way that Pkg prints it out (#61)

\",\"markup\":\"markdown\",\"type\":\"rendered\"},\"hash\":\"9f288cc4864dbfc2c82338caac0b1eb1fc7ff87f\",\"date\":\"2021-05-22T00:44:36+00:00\"}", + Codeberg.User => "{\"id\":1,\"login\":\"testuser\",\"login_name\":\"\",\"full_name\":\"Test User\",\"email\":\"test@example.org\",\"avatar_url\":\"https://codeberg.org/avatars/1\",\"html_url\":\"https://codeberg.org/testuser\",\"language\":\"\",\"is_admin\":false,\"last_login\":\"2024-01-01T00:00:00Z\",\"created\":\"2024-01-01T00:00:00Z\",\"restricted\":false,\"active\":true,\"prohibit_login\":false,\"location\":\"\",\"website\":\"\",\"description\":\"\",\"visibility\":\"public\",\"followers_count\":0,\"following_count\":0,\"starred_repos_count\":0,\"username\":\"testuser\"}", + Codeberg.Repo => "{\"id\":1,\"owner\":{\"id\":1,\"login\":\"testuser\",\"login_name\":\"\",\"full_name\":\"Test User\",\"email\":\"\",\"avatar_url\":\"\",\"html_url\":\"\",\"language\":\"\",\"is_admin\":false,\"last_login\":\"2024-01-01T00:00:00Z\",\"created\":\"2024-01-01T00:00:00Z\",\"restricted\":false,\"active\":true,\"prohibit_login\":false,\"location\":\"\",\"website\":\"\",\"description\":\"\",\"visibility\":\"public\",\"followers_count\":0,\"following_count\":0,\"starred_repos_count\":0,\"username\":\"testuser\"},\"name\":\"testrepo\",\"full_name\":\"testuser/testrepo\",\"description\":\"\",\"empty\":false,\"private\":false,\"fork\":false,\"template\":false,\"mirror\":false,\"size\":0,\"language\":\"\",\"languages_url\":\"\",\"html_url\":\"https://codeberg.org/testuser/testrepo\",\"url\":\"\",\"ssh_url\":\"\",\"clone_url\":\"https://codeberg.org/testuser/testrepo.git\",\"original_url\":\"\",\"website\":\"\",\"stars_count\":0,\"forks_count\":0,\"watchers_count\":0,\"open_issues_count\":0,\"open_pr_counter\":0,\"release_counter\":0,\"default_branch\":\"main\",\"archived\":false,\"created_at\":\"2024-01-01T00:00:00Z\",\"updated_at\":\"2024-01-01T00:00:00Z\",\"has_issues\":true,\"has_wiki\":true,\"has_pull_requests\":true,\"has_projects\":false,\"has_releases\":true,\"has_packages\":false,\"has_actions\":false,\"ignore_whitespace_conflicts\":false,\"allow_merge_commits\":true,\"allow_rebase\":true,\"allow_rebase_explicit\":true,\"allow_squash_merge\":true,\"allow_rebase_update\":true,\"default_delete_branch_after_merge\":false,\"default_merge_style\":\"merge\",\"default_allow_maintainer_edit\":false,\"avatar_url\":\"\",\"internal\":false,\"mirror_interval\":\"\"}", ] @testset "encoding" begin