├── author ├── sections │ ├── keys.yml │ ├── lint.yml │ ├── search.yml │ ├── version.yml │ ├── markdown.yml │ ├── statistics.yml │ ├── features.yml │ ├── namespaces.yml │ ├── settings.yml │ ├── todos.yml │ ├── licenses_templates.yml │ ├── system_hooks.yml │ ├── deployments.yml │ ├── events.yml │ ├── gitignores_templates.yml │ ├── gitlab_ci_ymls_templates.yml │ ├── sidekiq_metrics.yml │ ├── services.yml │ ├── remote_mirrors.yml │ ├── protected_tags.yml │ ├── wikis.yml │ ├── group_access_requests.yml │ ├── protected_branches.yml │ ├── broadcast_messages.yml │ ├── project_import_export.yml │ ├── releases.yml │ ├── project_access_requests.yml │ ├── snippets.yml │ ├── group_level_variables.yml │ ├── group_badges.yml │ ├── labels.yml │ ├── deploy_keys.yml │ ├── group_members.yml │ ├── project_level_variables.yml │ ├── repository_files.yml │ ├── environments.yml │ ├── project_badges.yml │ ├── page_domains.yml │ ├── project_members.yml │ ├── tags.yml │ ├── release_links.yml │ ├── repositories.yml │ ├── boards.yml │ ├── runners.yml │ ├── group_milestones.yml │ ├── notification_settings.yml │ ├── group_boards.yml │ ├── resource_label_events.yml │ ├── milestones.yml │ ├── project_snippets.yml │ ├── branches.yml │ ├── container_registry.yml │ ├── groups.yml │ ├── jobs.yml │ ├── commits.yml │ ├── pipelines.yml │ ├── custom_attributes.yml │ ├── pipeline_schedules.yml │ ├── notes.yml │ ├── issues.yml │ ├── pipeline_triggers.yml │ ├── projects.yml │ ├── users.yml │ ├── merge_requests.yml │ ├── award_emoji.yml │ └── discussions.yml ├── setup-test-container ├── README.pod ├── footer.pm ├── generate.pl ├── config.yml └── header.pm ├── minil.toml ├── .editorconfig ├── .mailmap ├── Build.PL ├── .gitignore ├── t ├── gitlab-api-v4.t ├── unit.t └── regression.t ├── cpanfile ├── lib └── GitLab │ └── API │ └── v4 │ ├── Mock.pm │ ├── Constants.pm │ ├── Mock │ ├── RESTClient.pm │ └── Engine.pm │ ├── Paginator.pm │ ├── WWWClient.pm │ ├── Config.pm │ └── RESTClient.pm ├── META.json ├── script └── gitlab-api-v4 ├── Changes └── LICENSE /author/sections/keys.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - key: key = GET keys/:key_id 3 | -------------------------------------------------------------------------------- /author/sections/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - lint: result = POST lint? 3 | -------------------------------------------------------------------------------- /author/sections/search.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - search: results = GET search? 3 | -------------------------------------------------------------------------------- /author/sections/version.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - version: version = GET version 3 | -------------------------------------------------------------------------------- /author/sections/markdown.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - markdown: html = POST markdown? 3 | -------------------------------------------------------------------------------- /author/sections/statistics.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - statistics: statistics = GET application/statistics 3 | -------------------------------------------------------------------------------- /author/sections/features.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - features: features = GET features 3 | - set_feature: feature = POST features/:name? 4 | -------------------------------------------------------------------------------- /minil.toml: -------------------------------------------------------------------------------- 1 | name = "GitLab-API-v4" 2 | authority = "cpan:BLUEFEET" 3 | markdown_maker = "Pod::Markdown::Github" 4 | license = "perl_5" 5 | -------------------------------------------------------------------------------- /author/sections/namespaces.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - namespaces: namespaces = GET namespaces? 3 | - namespace: namespace = GET namespaces/:namespace_id 4 | -------------------------------------------------------------------------------- /author/sections/settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - settings: settings = GET application/settings 3 | - update_settings: settings = PUT application/settings? 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{pm,pl,t}] 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /author/sections/todos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - todos: todos = GET todos? 3 | - mark_todo_done: todo = POST todos/:todo_id/mark_as_done 4 | - mark_all_todos_done: POST todos/mark_as_done 5 | -------------------------------------------------------------------------------- /author/sections/licenses_templates.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - license_templates: templates = GET templates/licenses? 3 | - license_template: template = GET templates/licenses/:template_key? 4 | -------------------------------------------------------------------------------- /author/sections/system_hooks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hooks: hooks = GET hooks? 3 | - create_hook: POST hooks? 4 | - test_hook: hook = GET hooks/:hook_id 5 | - delete_hook: DELETE hooks/:hook_id 6 | -------------------------------------------------------------------------------- /author/sections/deployments.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - deployments: deployments = GET projects/:project_id/deployments? 3 | - deployment: deployment = GET projects/:project_id/deployments/:deployment_id 4 | -------------------------------------------------------------------------------- /author/sections/events.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - all_events: events = GET events? 3 | - user_events: events = GET users/:user_id/events? 4 | - project_events: events = GET projects/:project_id/events? 5 | -------------------------------------------------------------------------------- /author/sections/gitignores_templates.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - gitignores_templates: templates = GET templates/gitignores? 3 | - gitignores_template: template = GET templates/gitignores/:template_key 4 | -------------------------------------------------------------------------------- /author/sections/gitlab_ci_ymls_templates.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - gitlab_ci_ymls_templates: templates = GET templates/gitlab_ci_ymls? 3 | - gitlab_ci_ymls_template: template = GET templates/gitlab_ci_ymls/:template_key 4 | -------------------------------------------------------------------------------- /author/sections/sidekiq_metrics.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - queue_metrics: metrics = GET sidekiq/queue_metrics 3 | - process_metrics: metrics = GET sidekiq/process_metrics 4 | - job_stats: stats = GET sidekiq/job_stats 5 | - compound_metrics: metrics = GET sidekiq/compound_metrics 6 | -------------------------------------------------------------------------------- /author/sections/services.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - project_service: service = GET projects/:project_id/services/:service_name 3 | - edit_project_service: PUT projects/:project_id/services/:service_name? 4 | - delete_project_service: DELETE projects/:project_id/services/:service_name 5 | -------------------------------------------------------------------------------- /author/sections/remote_mirrors.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - remote_mirrors: mirrors = GET projects/:project_id/remote_mirrors 3 | - create_remote_mirror: mirror = POST projects/:project_id/remote_mirrors? 4 | - edit_remote_mirror: mirror = PUT projects/:project_id/remote_mirrors/:mirror_id? 5 | 6 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Aran Clary Deltac Aran Clary Deltac 2 | Aran Clary Deltac Aran Deltac 3 | Luc Didry Luc Didry 4 | Aran Clary Deltac Aran Deltac 5 | -------------------------------------------------------------------------------- /author/sections/protected_tags.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - protected_tags: tags = GET projects/:project_id/protected_tags? 3 | - protected_tag: tag = GET projects/:project_id/protected_tags/:tag_name 4 | - protect_tag: tag = POST projects/:project_id/protected_tags? 5 | - unprotect_tag: DELETE projects/:project_id/protected_tags/:tag_name 6 | -------------------------------------------------------------------------------- /author/sections/wikis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - wiki_pages: pages = GET projects/:project_id/wikis? 3 | - wiki_page: pages = GET projects/:project_id/wikis/:slug 4 | - create_wiki_page: page = POST projects/:project_id/wikis? 5 | - edit_wiki_page: page = PUT projects/:project_id/wikis/:slug? 6 | - delete_wiki_page: DELETE projects/:project_id/wikis/:slug 7 | -------------------------------------------------------------------------------- /Build.PL: -------------------------------------------------------------------------------- 1 | # ========================================================================= 2 | # THIS FILE IS AUTOMATICALLY GENERATED BY MINILLA. 3 | # DO NOT EDIT DIRECTLY. 4 | # ========================================================================= 5 | 6 | use 5.008_001; 7 | use strict; 8 | 9 | use Module::Build::Tiny 0.035; 10 | 11 | Build_PL(); 12 | 13 | -------------------------------------------------------------------------------- /author/sections/group_access_requests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - group_access_requests: requests = GET groups/:group_id/access_requests? 3 | - request_group_access: request = POST groups/:group_id/access_requests 4 | - approve_group_access: request = PUT groups/:group_id/access_requests/:user_id/approve 5 | - deny_group_access: DELETE groups/:group_id/access_requests/:user_id 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.build/ 2 | /_build/ 3 | /Build 4 | /Build.bat 5 | /blib 6 | /Makefile 7 | /pm_to_blib 8 | 9 | /carton.lock 10 | /.carton/ 11 | /local/ 12 | 13 | nytprof.out 14 | nytprof/ 15 | 16 | cover_db/ 17 | 18 | *.bak 19 | *.old 20 | *~ 21 | *.swp 22 | *.o 23 | *.obj 24 | 25 | !LICENSE 26 | 27 | /_build_params 28 | 29 | MYMETA.* 30 | 31 | /GitLab-API-v4-* 32 | -------------------------------------------------------------------------------- /author/sections/protected_branches.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - protected_branches: branches = GET projects/:project_id/protected_branches? 3 | - protected_branch: branch = GET projects/:project_id/protected_branches/:branch_name 4 | - protect_branch: branch = POST projects/:project_id/protected_branches? 5 | - unprotect_branch: DELETE projects/:project_id/protected_branches/:branch_name 6 | -------------------------------------------------------------------------------- /author/sections/broadcast_messages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - broadcast_messages: messages = GET broadcast_messages? 3 | - broadcast_message: message = GET broadcast_messages/:message_id 4 | - create_broadcast_message: message = POST broadcast_messages? 5 | - edit_broadcast_message: message = PUT broadcast_messages/:message_id? 6 | - delete_broadcast_message: DELETE broadcast_messages/:message_id 7 | -------------------------------------------------------------------------------- /author/sections/project_import_export.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - schedule_project_export: POST projects/:project_id/export? 3 | - project_export_status: status = GET projects/:project_id/export 4 | - download_project_export: download = GET projects/:project_id/export/download 5 | - schedule_project_import: POST projects/import? 6 | - project_import_status: status = GET projects/:project_id/import 7 | -------------------------------------------------------------------------------- /author/sections/releases.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - releases: releases = GET projects/:project_id/releases? 3 | - release: release = GET projects/:project_id/releases/:tag_name 4 | - create_release: release = POST projects/:project_id/releases? 5 | - update_release: release = PUT projects/:project_id/releases/:tag_name? 6 | - delete_release: release = DELETE projects/:project_id/releases/:tag_name 7 | -------------------------------------------------------------------------------- /author/sections/project_access_requests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - project_access_requests: requests = GET projects/:project_id/access_requests? 3 | - request_project_access: request = POST projects/:project_id/access_requests 4 | - approve_project_access: request = PUT projects/:project_id/access_requests/:user_id/approve 5 | - deny_project_access: DELETE projects/:project_id/access_requests/:user_id 6 | -------------------------------------------------------------------------------- /author/sections/snippets.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - snippets: snippets = GET snippets 3 | - snippet: snippet = GET snippets/:snippet_id 4 | - create_snippet: snippet = POST snippets? 5 | - edit_snippet: snippet = PUT snippets/:snippet_id? 6 | - delete_snippet: DELETE snippets/:snippet_id 7 | - public_snippets: snippets = GET snippets/public? 8 | - snippet_user_agent_detail: user_agent = GET snippets/:snippet_id/user_agent_detail 9 | -------------------------------------------------------------------------------- /author/sections/group_level_variables.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - group_variables: variables = GET groups/:group_id/variables? 3 | - group_variable: variable = GET groups/:group_id/variables/:variable_key 4 | - create_group_variable: variable = POST groups/:group_id/variables? 5 | - edit_group_variable: variable = PUT groups/:group_id/variables/:variable_key? 6 | - delete_group_variable: DELETE groups/:group_id/variables/:variable_key 7 | -------------------------------------------------------------------------------- /author/sections/group_badges.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - group_badges: badges = GET groups/:group_id/badges 3 | - group_badge: badge = GET groups/:group_id/badges/:badge_id 4 | - create_group_badge: badge = POST groups/:group_id/badges? 5 | - edit_group_badge: badge = PUT groups/:group_id/badges/:badge_id? 6 | - delete_group_badge: DELETE groups/:group_id/badges/:badge_id 7 | - preview_group_badge: preview = GET groups/:group_id/badges/render? 8 | -------------------------------------------------------------------------------- /author/sections/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - labels: labels = GET projects/:project_id/labels? 3 | - create_label: label = POST projects/:project_id/labels? 4 | - delete_label: DELETE projects/:project_id/labels? 5 | - edit_label: label = PUT projects/:project_id/labels? 6 | - subscribe_to_label: label = POST projects/:project_id/labels/:label_id/subscribe 7 | - unsubscribe_from_label: POST projects/:project_id/labels/:label_id/unsubscribe 8 | -------------------------------------------------------------------------------- /author/sections/deploy_keys.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - all_deploy_keys: keys = GET deploy_keys? 3 | - deploy_keys: keys = GET projects/:project_id/deploy_keys? 4 | - deploy_key: key = GET projects/:project_id/deploy_keys/:key_id 5 | - create_deploy_key: key = POST projects/:project_id/deploy_keys? 6 | - delete_deploy_key: DELETE projects/:project_id/deploy_keys/:key_id 7 | - enable_deploy_key: key = POST projects/:project_id/deploy_keys/:key_id/enable 8 | -------------------------------------------------------------------------------- /author/sections/group_members.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - group_members: members = GET groups/:group_id/members? 3 | - all_group_members: members = GET groups/:group_id/members/all? 4 | - group_member: member = GET groups/:project_id/members/:user_id 5 | - add_group_member: member = POST groups/:group_id/members? 6 | - update_group_member: member = PUT groups/:group_id/members/:user_id? 7 | - remove_group_member: DELETE groups/:group_id/members/:user_id 8 | -------------------------------------------------------------------------------- /author/sections/project_level_variables.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - project_variables: variables = GET projects/:project_id/variables? 3 | - project_variable: variable = GET projects/:project_id/variables/:variable_key 4 | - create_project_variable: variable = POST projects/:project_id/variables? 5 | - edit_project_variable: variable = PUT projects/:project_id/variables/:variable_key? 6 | - delete_project_variable: DELETE projects/:project_id/variables/:variable_key 7 | -------------------------------------------------------------------------------- /author/sections/repository_files.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - file: file = GET projects/:project_id/repository/files/:file_path? 3 | - method: raw_file 4 | spec: content = GET projects/:project_id/repository/files/:file_path/raw? 5 | no_decode: true 6 | - create_file: POST projects/:project_id/repository/files/:file_path? 7 | - edit_file: PUT projects/:project_id/repository/files/:file_path? 8 | - delete_file: DELETE projects/:project_id/repository/files/:file_path? 9 | -------------------------------------------------------------------------------- /author/sections/environments.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - environments: environments = GET projects/:project_id/environments? 3 | - create_environment: environment = POST projects/:project_id/environments? 4 | - edit_environment: environment = PUT projects/:project_id/environments/:environments_id? 5 | - delete_environment: DELETE projects/:project_id/environments/:environment_id 6 | - stop_environment: environment = POST projects/:project_id/environments/:environment_id/stop 7 | -------------------------------------------------------------------------------- /author/sections/project_badges.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - project_badges: badges = GET projects/:project_id/badges 3 | - project_badge: badge = GET projects/:project_id/badges/:badge_id 4 | - create_project_badge: badge = POST projects/:project_id/badges? 5 | - edit_project_badge: badge = PUT projects/:project_id/badges/:badge_id? 6 | - delete_project_badge: DELETE projects/:project_id/badges/:badge_id 7 | - preview_project_badge: preview = GET projects/:project_id/badges/render? 8 | -------------------------------------------------------------------------------- /author/sections/page_domains.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - global_pages_domains: domains = GET pages/domains? 3 | - pages_domains: domains = GET projects/:project_id/pages/domains? 4 | - pages_domain: domain = GET projects/:project_id/pages/domains/:domain 5 | - create_pages_domain: domain = POST projects/:project_id/pages/domains? 6 | - edit_pages_domain: domain = PUT projects/:project_id/pages/domains/:domain? 7 | - delete_pages_domain: DELETE projects/:project_id/pages/domains/:domain 8 | -------------------------------------------------------------------------------- /author/sections/project_members.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - project_members: members = GET projects/:project_id/members? 3 | - all_project_members: members = GET projects/:project_id/members/all? 4 | - project_member: member = GET projects/:project_id/members/:user_id 5 | - add_project_member: member = POST projects/:project_id/members? 6 | - update_project_member: member = PUT projects/:project_id/members/:user_id? 7 | - remove_project_member: DELETE projects/:project_id/members/:user_id 8 | -------------------------------------------------------------------------------- /author/sections/tags.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - tags: tags = GET projects/:project_id/repository/tags? 3 | - tag: tag = GET projects/:project_id/repository/tags/:tag_name 4 | - create_tag: tag = POST projects/:project_id/repository/tags? 5 | - delete_tag: DELETE projects/:project_id/repository/tags/:tag_name 6 | - create_tag_release: release = POST projects/:project_id/repository/tags/:tag_name/release? 7 | - update_tag_release: release = PUT projects/:project_id/repository/tags/:tag_name/release? 8 | -------------------------------------------------------------------------------- /author/sections/release_links.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - release_links: links = GET projects/:project_id/releases/:tag_name/assets/links? 3 | - release_link: link = GET projects/:project_id/releases/:tag_name/assets/links/:link_id 4 | - create_release_link: link = POST projects/:project_id/releases/:tag_name/assets/links? 5 | - update_release_link: link = PUT projects/:project_id/releases/:tag_name/assets/links/:link_id? 6 | - delete_release_link: link = DELETE projects/:project_id/releases/:tag_name/assets/links/:link_id 7 | -------------------------------------------------------------------------------- /author/sections/repositories.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - tree: tree = GET projects/:project_id/repository/tree? 3 | - blob: blob = GET projects/:project_id/repository/blobs/:sha 4 | - raw_blob: raw_blob = GET projects/:project_id/repository/blobs/:sha/raw 5 | - method: archive 6 | spec: archive = GET projects/:project_id/repository/archive? 7 | no_decode: true 8 | - compare: comparison = GET projects/:project_id/repository/compare? 9 | - contributors: contributors = GET projects/:project_id/repository/contributors? 10 | -------------------------------------------------------------------------------- /author/sections/boards.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - project_boards: boards = GET projects/:project_id/boards? 3 | - project_board_lists: lists = GET projects/:project_id/boards/:board_id/lists? 4 | - project_board_list: list = GET projects/:project_id/boards/:board_id/lists/:list_id 5 | - create_project_board_list: list = POST projects/:project_id/boards/:board_id/lists? 6 | - edit_project_board_list: list = PUT projects/:project_id/boards/:board_id/lists/:list_id? 7 | - delete_project_board_list: DELETE projects/:project_id/boards/:board_id/lists/:list_id 8 | -------------------------------------------------------------------------------- /author/sections/runners.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - runners: runners = GET runners? 3 | - all_runners: runners = GET runners/all? 4 | - runner: runner = GET runners/:runner_id 5 | - update_runner: runner = PUT runners/:runner_id? 6 | - delete_runner: DELETE runners/:runner_id 7 | - runner_jobs: jobs = GET runners/:runner_id/jobs? 8 | - project_runners: runners = GET projects/:project_id/runners? 9 | - enable_project_runner: runner = POST projects/:project_id/runners? 10 | - disable_project_runner: runner = DELETE projects/:project_id/runners/:runner_id 11 | -------------------------------------------------------------------------------- /author/sections/group_milestones.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - group_milestones: milestones = GET groups/:group_id/milestones? 3 | - group_milestone: milestone = GET groups/:group_id/milestones/:milestone_id 4 | - create_group_milestone: milestone = POST groups/:group_id/milestones? 5 | - edit_group_milestone: milestone = PUT groups/:group_id/milestones/:milestone_id? 6 | - group_milestone_issues: issues = GET groups/:group_id/milestones/:milestone_id/issues? 7 | - group_milestone_merge_requests: merge_requests = GET groups/:group_id/milestones/:milestone_id/merge_requests? 8 | -------------------------------------------------------------------------------- /author/sections/notification_settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - global_notification_settings: settings = GET notification_settings 3 | - set_global_notification_settings: settings = PUT notification_settings? 4 | - group_notification_settings: settings = GET groups/:group_id/notification_settings 5 | - project_notification_settings: settings = GET projects/:project_id/notification_settings 6 | - set_group_notification_settings: settings = PUT groups/:group_id/notification_settings? 7 | - set_project_notification_settings: settings = PUT projects/:project_id/notification_settings? 8 | -------------------------------------------------------------------------------- /author/sections/group_boards.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - group_boards: boards = GET groups/:group_id/boards 3 | - group_board: board = GET groups/:group_id/boards/:board_id 4 | - group_board_lists: lists = GET groups/:group_id/boards/:board_id/lists 5 | - group_board_list: list = GET groups/:group_id/boards/:board_id/lists/:list_id 6 | - create_group_board_list: list = POST groups/:group_id/boards/:board_id/lists? 7 | - edit_group_board_list: list = PUT groups/:group_id/boards/:board_id/lists/:list_id? 8 | - delete_group_board_list: DELETE groups/:group_id/boards/:board_id/lists/:list_id 9 | -------------------------------------------------------------------------------- /author/sections/resource_label_events.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - issue_resource_label_events: events = GET projects/:project_id/issues/:issue_iid/resource_label_events 3 | - issue_resource_label_event: event = GET projects/:project_id/issues/:issue_iid/resource_label_events/:resource_label_event_id 4 | 5 | - merge_request_resource_label_events: events = GET projects/:project_id/merge_requests/:merge_request_iid/resource_label_events 6 | - merge_request_resource_label_event: event = GET projects/:project_id/merge_requests/:merge_request_iid/resource_label_events/:resource_label_event_id 7 | -------------------------------------------------------------------------------- /author/sections/milestones.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - project_milestones: milestones = GET projects/:project_id/milestones? 3 | - project_milestone: milestone = GET projects/:project_id/milestones/:milestone_id 4 | - create_project_milestone: milestone = POST projects/:project_id/milestones? 5 | - edit_project_milestone: milestone = PUT projects/:project_id/milestones/:milestone_id? 6 | - project_milestone_issues: issues = GET projects/:project_id/milestones/:milestone_id/issues? 7 | - project_milestone_merge_requests: merge_requests = GET projects/:project_id/milestones/:milestone_id/merge_requests? 8 | -------------------------------------------------------------------------------- /author/sections/project_snippets.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - project_snippets: snippets = GET projects/:project_id/snippets? 3 | - project_snippet: snippet = GET projects/:project_id/snippets/:snippet_id 4 | - create_project_snippet: POST projects/:project_id/snippets? 5 | - edit_project_snippet: PUT projects/:project_id/snippets/:snippet_id? 6 | - delete_project_snippet: DELETE projects/:project_id/snippets/:snippet_id 7 | - project_snippet_content: content = GET projects/:project_id/snippets/:snippet_id/raw 8 | - project_snippet_user_agent_detail: user_agent = GET projects/:project_id/snippets/:snippet_id/user_agent_detail 9 | -------------------------------------------------------------------------------- /author/sections/branches.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - branches: branches = GET projects/:project_id/repository/branches? 3 | - branch: branch = GET projects/:project_id/repository/branches/:branch_name 4 | - create_branch: branch = POST projects/:project_id/repository/branches? 5 | - delete_branch: DELETE projects/:project_id/repository/branches/:branch_name 6 | - delete_merged_branches: DELETE projects/:project_id/repository/merged_branches 7 | 8 | # These are not implemented as they are already deprecated in the v4 API docs. 9 | #- protect_branch: PUT /projects/:project_id/repository/branches/:branch_name/protect? 10 | #- unprotect_branch: PUT /projects/:project_id/repository/branches/:branch_name/unprotect 11 | -------------------------------------------------------------------------------- /t/gitlab-api-v4.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strictures 2; 3 | 4 | use Test2::Require::AuthorTesting; 5 | use Test2::V0; 6 | 7 | use IPC::Cmd qw(); 8 | use JSON::MaybeXS qw(); 9 | 10 | my $json = JSON::MaybeXS->new(allow_nonref => 1); 11 | 12 | my $project = run('create-project', 'name:test-gitlab-api-v4'); 13 | run('delete-project', $project->{id}); 14 | 15 | ok( 1, 'made it to the end' ); 16 | 17 | done_testing; 18 | 19 | sub run { 20 | my($ok, $error, $full, $stdout, $stderr) = 21 | IPC::Cmd::run( command => [$^X, '-I', 'lib', 'script/gitlab-api-v4', @_] ); 22 | 23 | if ($ok) { 24 | $stdout = join('',@$stdout); 25 | return $json->decode( $stdout ); 26 | } 27 | 28 | die join('', @$stderr) . $error; 29 | } 30 | 31 | 1; 32 | -------------------------------------------------------------------------------- /author/sections/container_registry.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - registry_repositories_in_project: registry_repositories = GET projects/:project_id/registry/repositories? 3 | - registry_repositories_in_group: registry_repositories = GET groups/:group_id/registry/repositories? 4 | - delete_registry_repository: DELETE projects/:project_id/registry/repositories/:repository_id 5 | - registry_repository_tags: tags = GET projects/:project_id/registry/repositories/:repository_id/tags 6 | - registry_repository_tag: tag = GET projects/:project_id/registry/repositories/:repository_id/tags/:tag_name 7 | - delete_registry_repository_tag: DELETE projects/:project_id/registry/repositories/:repository_id/tags/:tag_name 8 | - bulk_delete_registry_repository_tags: DELETE projects/:project_id/registry/repositories/:repository_id/tags? 9 | -------------------------------------------------------------------------------- /author/setup-test-container: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #docker stop test-gitlab-api-v4 4 | 5 | sleep 5 6 | 7 | #docker run -d --rm \ 8 | # -p 80:80 \ 9 | # --name test-gitlab-api-v4 \ 10 | # gitlab/gitlab-ce:16.0.3-ce.0 11 | 12 | echo 1. Open you browser to: http://localhost \(it may take several minutes for it to respond\) 13 | echo 2. Get the root password with: sudo docker exec -it test-gitlab-api-v4 grep 'Password:' /etc/gitlab/initial_root_password 14 | echo 3. Then login and goto: http://localhost/-/profile/personal_access_tokens 15 | echo 4. Create a new private token \(use this token below\). 16 | echo 5. Then set the following env vars: 17 | echo export GITLAB_API_V4_URL=http://localhost/api/v4 18 | echo export GITLAB_API_V4_PRIVATE_TOKEN= 19 | echo export GITLAB_API_V4_ROOT_PASSWORD= 20 | echo 6. You should now be able to run: t/regression.t 21 | -------------------------------------------------------------------------------- /author/sections/groups.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - groups: groups = GET groups? 3 | - group_subgroups: subgroups = GET groups/:group_id/subgroups? 4 | - group_projects: projects = GET groups/:group_id/projects? 5 | - group: group = GET groups/:group_id? 6 | - create_group: POST groups? 7 | - transfer_project_to_group: POST groups/:group_id/projects/:project_id 8 | - edit_group: group = PUT groups/:group_id? 9 | - delete_group: DELETE groups/:group_id 10 | - sync_group_with_ldap: POST groups/:group_id/ldap_sync 11 | - create_ldap_group_link: POST groups/:group_id/ldap_group_links? 12 | - delete_ldap_group_link: DELETE groups/:group_id/ldap_group_links/:cn 13 | - delete_ldap_provider_group_link: DELETE groups/:group_id/ldap_group_links/:provider/:cn 14 | - share_group_with_group: POST groups/:group_id/share? 15 | - unshare_group_with_group: DELETE groups/:group_id/share/:shared_with_group_id -------------------------------------------------------------------------------- /author/sections/jobs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - jobs: jobs = GET projects/:project_id/jobs? 3 | - pipeline_jobs: jobs = GET projects/:project_id/pipelines/:pipeline_id/jobs? 4 | - job: job = GET projects/:project_id/jobs/:job_id 5 | - job_artifacts: artifacts = GET projects/:project_id/jobs/:job_id/artifacts 6 | - job_artifacts_archive: archive = GET projects/:project_id/jobs/artifacts/:ref_name/download? 7 | - job_artifacts_file: file = GET projects/:project_id/jobs/:job_id/artifacts/:artifact_path 8 | - job_trace_file: file = GET projects/:project_id/jobs/:job_id/trace 9 | - cancel_job: job = POST projects/:project_id/jobs/:job_id/cancel 10 | - retry_job: job = POST projects/:project_id/jobs/:job_id/retry 11 | - erase_job: job = POST projects/:project_id/jobs/:job_id/erase 12 | - keep_job_artifacts: job = POST projects/:project_id/jobs/:job_id/artifacts/keep 13 | - play_job: job = POST projects/:project_id/jobs/:job_id/play 14 | -------------------------------------------------------------------------------- /author/sections/commits.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - commits: commits = GET projects/:project_id/repository/commits? 3 | - create_commit: commit = POST projects/:project_id/repository/commits? 4 | - commit: commit = GET projects/:project_id/repository/commits/:commit_sha 5 | - commit_refs: refs = GET projects/:project_id/repository/commits/:commit_sha/refs? 6 | - cherry_pick_commit: commit = POST projects/:project_id/repository/commits/:commit_sha/cherry_pick? 7 | - commit_diff: diff = GET projects/:project_id/repository/commits/:commit_sha/diff? 8 | - commit_comments: comments = GET projects/:project_id/repository/commits/:commit_sha/comments? 9 | - create_commit_comment: POST projects/:project_id/repository/commits/:commit_sha/comments? 10 | - commit_statuses: build_statuses = GET projects/:project_id/repository/commits/:commit_sha/statuses? 11 | - create_commit_status: build_status = POST projects/:project_id/statuses/:commit_sha? 12 | -------------------------------------------------------------------------------- /author/sections/pipelines.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - pipelines: pipelines = GET projects/:project_id/pipelines? 3 | - pipeline: pipeline = GET projects/:project_id/pipelines/:pipeline_id 4 | - method: create_pipeline 5 | spec: pipeline = POST projects/:project_id/pipeline? 6 | note: | 7 | Git ref (branch or tag) name must be specified in the C field of the 8 | C<%params> hash. It's also possible to pass variables to a pipeline in 9 | the C field like in the following example: 10 | 11 | my $pipeline = $api->create_pipeline( 12 | $project_id, 13 | { 14 | 'ref' => 'master', 15 | variables => [ 16 | { 'key' => 'VARIABLE1', 'value' => 'VALUE1' }, 17 | { 'key' => 'VARIABLE2', 'value' => 'VALUE2' }, 18 | ], 19 | }, 20 | ); 21 | - retry_pipeline_jobs: pipeline = POST projects/:project_id/pipelines/:pipeline_id/retry 22 | - cancel_pipeline_jobs: pipeline = POST projects/:project_id/pipelines/:pipeline_id/cancel 23 | - delete_pipeline: DELETE projects/:project_id/pipelines/:pipeline_id 24 | -------------------------------------------------------------------------------- /author/sections/custom_attributes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - custom_user_attributes: attributes = GET users/:user_id/custom_attributes 3 | - custom_group_attributes: attributes = GET groups/:group_id/custom_attributes 4 | - custom_project_attributes: attributes = GET projects/:project_id/custom_attributes 5 | - custom_user_attribute: attribute = GET users/:user_id/custom_attributes/:attribute_key 6 | - custom_group_attribute: attribute = GET groups/:group_id/custom_attributes/:attribute_key 7 | - custom_project_attribute: attribute = GET projects/:project_id/custom_attributes/:attribute_key 8 | - set_custom_user_attribute: attribute = PUT users/:user_id/custom_attributes/:attribute_key? 9 | - set_custom_group_attribute: attribute = PUT groups/:group_id/custom_attributes/:attribute_key? 10 | - set_custom_project_attribute: attribute = PUT projects/:project_id/custom_attributes/:attribute_key? 11 | - delete_custom_user_attribute: DELETE users/:user_id/custom_attributes/:attribute_key 12 | - delete_custom_group_attribute: DELETE groups/:group_id/custom_attributes/:attribute_key 13 | - delete_custom_project_attribute: DELETE projects/:project_id/custom_attributes/:attribute_key 14 | -------------------------------------------------------------------------------- /author/sections/pipeline_schedules.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - pipeline_schedules: schedules = GET projects/:project_id/pipeline_schedules? 3 | - pipeline_schedule: schedule = GET projects/:project_id/pipeline_schedules/:pipeline_schedule_id 4 | - create_pipeline_schedule: schedule = POST projects/:project_id/pipeline_schedules? 5 | - edit_pipeline_schedule: schedule = PUT projects/:project_id/pipeline_schedules/:pipeline_schedule_id? 6 | - take_ownership_of_pipeline_schedule: schedule = POST projects/:project_id/pipeline_schedules/:pipeline_schedule_id/take_ownership 7 | - run_pipeline_schedule: variable = POST projects/:project_id/pipeline_schedules/:pipeline_schedule_id/play 8 | - delete_pipeline_schedule: schedule = DELETE projects/:project_id/pipeline_schedules/:pipeline_schedule_id 9 | - create_pipeline_schedule_variable: variable = POST projects/:project_id/pipeline_schedules/:pipeline_schedule_id/variables? 10 | - edit_pipeline_schedule_variable: variable = PUT projects/:project_id/pipeline_schedules/:pipeline_schedule_id/variables/:variable_key? 11 | - delete_pipeline_schedule_variable: variable = DELETE projects/:project_id/pipeline_schedules/:pipeline_schedule_id/variables/:variable_key 12 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | requires 'perl' => '5.010001'; 2 | 3 | # Common modules. 4 | requires 'Moo' => '2.003000'; 5 | requires 'strictures' => '2.000003'; 6 | requires 'namespace::clean' => '0.27'; 7 | requires 'Types::Standard' => '1.002001'; 8 | requires 'Types::Common::String' => '1.002001'; 9 | requires 'Types::Common::Numeric' => '1.002001'; 10 | requires 'Log::Any' => '1.703'; 11 | requires 'Carp'; 12 | requires 'JSON::MaybeXS' => '1.003007'; 13 | 14 | # Used by GitLab::API::v4::RESTClient. 15 | requires 'HTTP::Tiny' => '0.059'; 16 | requires 'HTTP::Tiny::Multipart' => '0.05'; 17 | requires 'URI' => '1.62'; 18 | requires 'URI::Escape' => '1.72'; 19 | 20 | # Used by GitLab::API::v4::WWWClient. 21 | requires 'List::Util'; 22 | 23 | # Used by GitLab::API::v4::Constants. 24 | requires 'Const::Fast' => '0.014'; 25 | requires 'Exporter'; 26 | 27 | # Used by gitlab-api-v4 and/or GitLab::API::v4::Config. 28 | requires 'Try::Tiny' => '0.28'; 29 | requires 'Getopt::Long'; 30 | requires 'Pod::Usage'; 31 | requires 'Log::Any::Adapter' => '1.703'; 32 | requires 'Log::Any::Adapter::Screen' => '0.13'; 33 | requires 'Path::Tiny' => '0.079'; 34 | requires 'IO::Prompter' => '0.004014'; 35 | 36 | test_requires 'Test2::V0' => '0.000094'; 37 | test_requires 'Log::Any::Adapter::TAP' => '0.003003'; 38 | test_requires 'MIME::Base64' => '3.15'; 39 | 40 | -------------------------------------------------------------------------------- /author/sections/notes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - issue_notes: notes = GET projects/:project_id/issues/:issue_iid/notes? 3 | - issue_note: note = GET projects/:project_id/issues/:issue_iid/notes/:note_id 4 | - create_issue_note: note = POST projects/:project_id/issues/:issue_iid/notes? 5 | - edit_issue_note: PUT projects/:project_id/issues/:issue_iid/notes/:note_id? 6 | - delete_issue_note: DELETE projects/:project_id/issues/:issue_iid/notes/:note_id 7 | 8 | - snippet_notes: notes = GET projects/:project_id/snippets/:snippet_id/notes? 9 | - snippet_note: note = GET projects/:project_id/snippets/:snippet_id/notes/:note_id 10 | - create_snippet_note: note = POST projects/:project_id/snippets/:snippet_id/notes? 11 | - edit_snippet_note: PUT projects/:project_id/snippets/:snippet_id/notes/:note_id? 12 | - delete_snippet_note: DELETE projects/:project_id/snippets/:snippet_id/notes/:note_id 13 | 14 | - merge_request_notes: notes = GET projects/:project_id/merge_requests/:merge_request_iid/notes? 15 | - merge_request_note: note = GET projects/:project_id/merge_requests/:merge_request_iid/notes/:note_id 16 | - create_merge_request_note: note = POST projects/:project_id/merge_requests/:merge_request_iid/notes? 17 | - edit_merge_request_note: PUT projects/:project_id/merge_requests/:merge_request_iid/notes/:note_id? 18 | - delete_merge_request_note: DELETE projects/:project_id/merge_requests/:merge_request_iid/notes/:note_id 19 | -------------------------------------------------------------------------------- /author/sections/issues.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - global_issues: issues = GET issues? 3 | - group_issues: issues = GET groups/:group_id/issues? 4 | - issues: issues = GET projects/:project_id/issues? 5 | - issue: issue = GET projects/:project_id/issues/:issue_iid 6 | - create_issue: issue = POST projects/:project_id/issues? 7 | - edit_issue: issue = PUT projects/:project_id/issues/:issue_iid? 8 | - delete_issue: DELETE projects/:project_id/issues/:issue_iid 9 | - move_issue: issue = POST projects/:project_id/issues/:issue_iid/move? 10 | - subscribe_to_issue: issue = POST projects/:project_id/issues/:issue_iid/subscribe 11 | - unsubscribe_from_issue: issue = POST projects/:project_id/issues/:issue_iid/unsubscribe 12 | - create_issue_todo: todo = POST projects/:project_id/issues/:issue_iid/todo 13 | - set_issue_time_estimate: tracking = POST projects/:project_id/issues/:issue_iid/time_estimate? 14 | - reset_issue_time_estimate: tracking = POST projects/:project_id/issues/:issue_iid/reset_time_estimate 15 | - add_issue_spent_time: tracking = POST projects/:project_id/issues/:issue_iid/add_spent_time? 16 | - reset_issue_spent_time: tracking = POST projects/:project_id/issues/:issue_iid/reset_spent_time 17 | - issue_time_stats: tracking = GET projects/:project_id/issues/:issue_iid/time_stats 18 | - issue_closed_by: merge_requests = GET projects/:project_id/issues/:issue_iid/closed_by 19 | - issue_user_agent_detail: user_agent = GET projects/:project_id/issues/:issue_iid/user_agent_detail 20 | -------------------------------------------------------------------------------- /author/README.pod: -------------------------------------------------------------------------------- 1 | 2 | =pod 3 | 4 | The C directory contains the logic for generating the v4.pm 5 | module. 6 | 7 | =head1 DATA FILES 8 | 9 | The C drives the order and configuration of 10 | individual sections. 11 | 12 | The YAML files in the C directory have lines 13 | like this: 14 | 15 | - SUB: HTTP_METHOD PATH 16 | - SUB: RETURN = HTTP_METHOD PATH? 17 | 18 | =head2 SUB 19 | 20 | The name of the subroutine that will be created in v4.pm. 21 | 22 | =head2 RETURN 23 | 24 | The name of the return variable if any. This is used as part of 25 | generating the documentation. If no return variable is declared 26 | then the subroutine will return an empty list no matter what 27 | the API response returns. 28 | 29 | =head2 HTTP_METHOD 30 | 31 | One of GET, POST, PUT, or DELETE. 32 | 33 | =head2 PATH 34 | 35 | The path to the API endpoint (after the /api/v4 bit). 36 | The path is a list of static and dynamic strings separate by 37 | forward slashes. So, if you have: 38 | 39 | /foo/:bar/baz 40 | 41 | Then the API subroutine would require one argument which would 42 | be injected in place of C<:bar>. 43 | 44 | =head2 ? 45 | 46 | If the PATH ends with a C then this signifies that the API 47 | endpoint accepts query parameters. 48 | 49 | =head1 GENERATING 50 | 51 | To generate C do this: 52 | 53 | cd author 54 | perl generate.pl > ../lib/GitLab/API/v4.pm 55 | 56 | =cut 57 | 58 | -------------------------------------------------------------------------------- /author/sections/pipeline_triggers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - triggers: triggers = GET projects/:project_id/triggers? 3 | - trigger: trigger = GET projects/:project_id/triggers/:trigger_id 4 | - create_trigger: trigger = POST projects/:project_id/triggers? 5 | - edit_trigger: trigger = PUT projects/:project_id/triggers/:trigger_id? 6 | - take_ownership_of_trigger: trigger = POST projects/:project_id/triggers/:trigger_id/take_ownership 7 | - delete_trigger: DELETE projects/:project_id/triggers/:trigger_id 8 | - method: trigger_pipeline 9 | spec: pipeline = POST projects/:project_id/trigger/pipeline? 10 | note: | 11 | The API authentication token (L or L 12 | parameters in a constructor) is not needed when using this method, however 13 | You must pass trigger token (generated at the trigger creation) as C 14 | field and git ref name as C field in the C<%params> hash. You can also 15 | pass variables to be set in a pipeline in the C field. Example: 16 | 17 | my $pipeline = $api->trigger_pipeline( 18 | $project_id, 19 | { 20 | token => 'd69dba9162ab6ac72fa0993496286ada', 21 | 'ref' => 'master', 22 | variables => { 23 | variable1 => 'value1', 24 | variable2 => 'value2', 25 | }, 26 | }, 27 | ); 28 | 29 | Read more at L. 30 | -------------------------------------------------------------------------------- /lib/GitLab/API/v4/Mock.pm: -------------------------------------------------------------------------------- 1 | package GitLab::API::v4::Mock; 2 | our $VERSION = '0.27'; 3 | 4 | =encoding utf8 5 | 6 | =head1 NAME 7 | 8 | GitLab::API::v4::Mock - Mock API object for testing. 9 | 10 | =head1 SYNOPSIS 11 | 12 | use GitLab::API::v4::Mock; 13 | 14 | my $api = GitLab::API::v4::Mock->new(); 15 | 16 | =head1 DESCRIPTION 17 | 18 | This module is a subclass of L. It modifies 19 | it to mock the REST client via L. 20 | 21 | This module is meant to be used for writing unit tests. 22 | 23 | =cut 24 | 25 | use GitLab::API::v4::Mock::RESTClient; 26 | 27 | use Moo; 28 | use strictures 2; 29 | use namespace::clean; 30 | 31 | extends 'GitLab::API::v4'; 32 | 33 | =head1 ATTRIBUTES 34 | 35 | =head2 url 36 | 37 | This attribute is altered from L to default 38 | to C and to not be required. 39 | 40 | =cut 41 | 42 | has '+url' => ( 43 | required => 0, 44 | default => 'https://example.com/api/v4', 45 | ); 46 | 47 | =head2 rest_client_class 48 | 49 | This attribute is altered from L 50 | to default to L. 51 | 52 | =cut 53 | 54 | sub _build_rest_client_class { 55 | return 'GitLab::API::v4::Mock::RESTClient'; 56 | } 57 | 58 | 1; 59 | __END__ 60 | 61 | =head1 SUPPORT 62 | 63 | See L. 64 | 65 | =head1 AUTHORS 66 | 67 | See L. 68 | 69 | =head1 LICENSE 70 | 71 | See L. 72 | 73 | =cut 74 | 75 | -------------------------------------------------------------------------------- /t/unit.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strictures 2; 3 | 4 | use Test2::V0; 5 | 6 | use GitLab::API::v4::Mock; 7 | 8 | subtest users => sub{ 9 | my @users = (); 10 | my $next_id = 1; 11 | 12 | my $api = GitLab::API::v4::Mock->new(); 13 | 14 | my @expected; 15 | is( $api->users(), \@expected, 'users is empty' ); 16 | 17 | $api->create_user({}); 18 | push @expected, {id=>1}; 19 | is( $api->users(), \@expected, 'one user created' ); 20 | 21 | $api->create_user({}); 22 | push @expected, {id=>2}; 23 | $api->create_user({}); 24 | push @expected, {id=>3}; 25 | is( $api->users(), \@expected, 'two more users created' ); 26 | 27 | $api->edit_user( 3, {name=>'foo'}); 28 | $expected[2]->{name} = 'foo'; 29 | is( $api->users(), \@expected, 'user was updated' ); 30 | 31 | $api->delete_user( 2 ); 32 | splice @expected, 1, 1; 33 | is( $api->users(), \@expected, 'user was deleted' ); 34 | }; 35 | 36 | subtest reqres => sub{ 37 | my $api = GitLab::API::v4::Mock->new(); 38 | 39 | is( $api->rest_client->http_tiny_request(), undef, 'no request' ); 40 | is( $api->rest_client->http_tiny_response(), undef, 'no response' ); 41 | 42 | $api->users(); 43 | 44 | is( 45 | $api->rest_client->http_tiny_request(), 46 | [ 'GET', 'https://example.com/api/v4/users', {headers=>{}} ], 47 | 'recorded request arrayref looks great', 48 | ); 49 | is( 50 | $api->rest_client->http_tiny_response(), 51 | { success=>1, status=>200, content=>'[]' }, 52 | 'recorded response hashref looks great', 53 | ); 54 | }; 55 | 56 | done_testing; 57 | -------------------------------------------------------------------------------- /author/sections/projects.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - projects: projects = GET projects? 3 | - user_projects: projects = GET users/:user_id/projects? 4 | - project: project = GET projects/:project_id? 5 | - project_users: users = GET projects/:project_id/users? 6 | - create_project: project = POST projects? 7 | - create_project_for_user: POST projects/user/:user_id? 8 | - edit_project: PUT projects/:project_id? 9 | - fork_project: POST projects/:project_id/fork? 10 | - project_forks: forks = GET projects/:project_id/forks? 11 | - start_project: project = POST projects/:project_id/star 12 | - unstar_project: project = POST projects/:project_id/unstar 13 | - project_languages: languages = GET projects/:project_id/languages 14 | - archive_project: project = POST projects/:project_id/archive 15 | - unarchive_project: project = POST projects/:project_id/unarchive 16 | - delete_project: DELETE projects/:project_id 17 | - method: upload_file_to_project 18 | spec: upload = POST projects/:project_id/uploads? 19 | note: The C parameter must point to a readable file on the local filesystem. 20 | - share_project_with_group: POST projects/:project_id/share? 21 | - unshare_project_with_group: DELETE projects/:project_id/share/:group_id 22 | - project_hooks: hooks = GET projects/:project_id/hooks 23 | - project_hook: hook = GET projects/:project_id/hooks/:hook_id 24 | - create_project_hook: hook = POST projects/:project_id/hooks? 25 | - edit_project_hook: hook = PUT projects/:project_id/hooks/:hook_id? 26 | - delete_project_hook: DELETE projects/:project_id/hooks/:hook_id 27 | - set_project_fork: POST projects/:project_id/fork/:from_project_id 28 | - clear_project_fork: DELETE projects/:project_id/fork 29 | - start_housekeeping: POST projects/:project_id/housekeeping 30 | - transfer_project_to_namespace: PUT projects/:project_id/transfer? 31 | # TODO: Support undecoded responses, maybe more. 32 | #- download_project_snapshot: snapshot = GET projects/:project_id/snapshot 33 | -------------------------------------------------------------------------------- /author/footer.pm: -------------------------------------------------------------------------------- 1 | 1; 2 | __END__ 3 | 4 | =head1 CONTRIBUTING 5 | 6 | This module is auto-generated from a set of YAML files defining the 7 | interface of GitLab's API. If you'd like to contribute to this module 8 | then please feel free to make a 9 | L 10 | and submit a pull request, just make sure you edit the files in the 11 | C directory instead of C directly. 12 | 13 | Please see 14 | L 15 | for more information. 16 | 17 | Alternatively, you can 18 | L. 19 | 20 | =head1 SUPPORT 21 | 22 | Please submit bugs and feature requests to the 23 | GitLab-API-v4 GitHub issue tracker: 24 | 25 | L 26 | 27 | =head1 AUTHOR 28 | 29 | Aran Clary Deltac 30 | 31 | =head1 CONTRIBUTORS 32 | 33 | Dotan Dimet 34 | Nigel Gregoire 35 | trunov-ms 36 | Marek R. Sotola 37 | José Joaquín Atria 38 | Dave Webb 39 | Simon Ruderich 40 | royce55 41 | gregor herrmann 42 | Luc Didry 43 | Kieren Diment 44 | Dmitry Frolov 45 | Thomas Klausner 46 | Graham Knop 47 | Stig Palmquist 48 | Dan Book 49 | James Wright 50 | Jonathan Taylor 51 | g0t mi1k 52 | 53 | =head1 LICENSE 54 | 55 | This library is free software; you can redistribute it and/or modify 56 | it under the same terms as Perl itself. 57 | 58 | =cut 59 | 60 | -------------------------------------------------------------------------------- /author/sections/users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - users: users = GET users? 3 | - user: user = GET users/:user_id 4 | - create_user: POST users? 5 | - edit_user: PUT users/:user_id? 6 | - delete_user: DELETE users/:user_id? 7 | - current_user: user = GET user 8 | - current_user_ssh_keys: keys = GET user/keys? 9 | - user_ssh_keys: keys = GET users/:user_id/keys? 10 | - user_ssh_key: key = GET user/keys/:key_id 11 | - create_current_user_ssh_key: POST user/keys? 12 | - create_user_ssh_key: POST users/:user_id/keys? 13 | - delete_current_user_ssh_key: DELETE user/keys/:key_id 14 | - delete_user_ssh_key: DELETE users/:user_id/keys/:key_id 15 | - current_user_gpg_keys: keys = GET user/gpg_keys? 16 | - current_user_gpg_key: key = GET user/gpg_keys/:key_id 17 | - create_current_user_gpg_key: POST user/gpg_keys? 18 | - delete_current_user_gpg_key: DELETE user/gpg_keys/:key_id 19 | - user_gpg_keys: keys = GET users/:user_id/gpg_keys? 20 | - user_gpg_key: key = GET users/:user_id/gpg_keys/:key_id 21 | - create_user_gpg_key: keys = POST users/:user_id/gpg_keys? 22 | - delete_user_gpg_key: DELETE users/:user_id/gpg_keys/:key_id 23 | - current_user_emails: emails = GET user/emails? 24 | - user_emails: emails = GET users/:user_id/emails? 25 | - current_user_email: email = GET user/emails/:email_id 26 | - create_current_user_email: email = POST user/emails? 27 | - create_user_email: email = POST users/:user_id/emails? 28 | - delete_current_user_email: DELETE user/emails/:email_id 29 | - delete_user_email: DELETE users/:user_id/emails/:email_id 30 | - block_user: success = POST users/:user_id/block 31 | - unblock_user: success = POST users/:user_id/unblock 32 | - approve_user: POST users/:user_id/approve 33 | - reject_user: POST users/:user_id/reject 34 | - activate_user: POST users/:user_id/activate 35 | - deactivate_user: POST users/:user_id/deactivate 36 | - ban_user: POST users/:user_id/ban 37 | - unban_user: POST users/:user_id/unban 38 | - user_impersonation_tokens: tokens = GET users/:user_id/impersonation_tokens? 39 | - user_impersonation_token: token = GET users/:user_id/impersonation_tokens/:impersonation_token_id 40 | - create_user_impersonation_token: token = POST users/:user_id/impersonation_tokens? 41 | - delete_user_impersonation_token: DELETE users/:user_id/impersonation_tokens/:impersonation_token_id 42 | - all_user_activities: activities = GET user/activities? 43 | - user_memberships: memberships = GET users/:user_id/memberships? 44 | -------------------------------------------------------------------------------- /author/sections/merge_requests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - global_merge_requests: merge_requests = GET merge_requests? 3 | - merge_requests: merge_requests = GET projects/:project_id/merge_requests? 4 | - merge_request: merge_request = GET projects/:project_id/merge_requests/:merge_request_iid 5 | - merge_request_commits: commits = GET projects/:project_id/merge_requests/:merge_request_iid/commits 6 | - merge_request_with_changes: merge_request = GET projects/:project_id/merge_requests/:merge_request_iid/changes 7 | - create_merge_request: merge_request = POST projects/:project_id/merge_requests? 8 | - edit_merge_request: merge_request = PUT projects/:project_id/merge_requests/:merge_request_iid? 9 | - delete_merge_request: DELETE projects/:project_id/merge_requests/:merge_request_iid 10 | - accept_merge_request: merge_request = PUT projects/:project_id/merge_requests/:merge_request_iid/merge? 11 | - approve_merge_request: merge_request = POST projects/:project_id/merge_requests/:merge_request_iid/approve? 12 | - unapprove_merge_request: merge_request = POST projects/:project_id/merge_requests/:merge_request_iid/unapprove? 13 | - cancel_merge_when_pipeline_succeeds: merge_request = PUT projects/:project_id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds 14 | - merge_request_closes_issues: issues = GET projects/:project_id/merge_requests/:merge_request_iid/closes_issues? 15 | - subscribe_to_merge_request: merge_request = POST projects/:project_id/merge_requests/:merge_request_iid/subscribe 16 | - unsubscribe_from_merge_request: merge_request = POST projects/:project_id/merge_requests/:merge_request_iid/unsubscribe 17 | - create_merge_request_todo: todo = POST projects/:project_id/merge_requests/:merge_request_iid/todo 18 | - merge_request_diff_versions: versions = GET projects/:project_id/merge_requests/:merge_request_iid/versions 19 | - merge_request_diff_version: version = GET projects/:project_id/merge_requests/:merge_request_iid/versions/:version_id 20 | - set_merge_request_time_estimate: tracking = POST projects/:project_id/merge_requests/:merge_request_iid/time_estimate? 21 | - reset_merge_request_time_estimate: tracking = POST projects/:project_id/merge_requests/:merge_request_iid/reset_time_estimate 22 | - add_merge_request_spent_time: tracking = POST projects/:project_id/merge_requests/:merge_request_iid/add_spent_time? 23 | - reset_merge_request_spent_time: tracking = POST projects/:project_id/merge_requests/:merge_request_iid/reset_spent_time 24 | - merge_request_time_stats: tracking = GET projects/:project_id/merge_requests/:merge_request_iid/time_stats 25 | -------------------------------------------------------------------------------- /author/sections/award_emoji.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - issue_award_emojis: award_emojis = GET projects/:project_id/issues/:issue_iid/award_emoji? 3 | - merge_request_award_emojis: award_emojis = GET projects/:project_id/merge_requests/:merge_request_iid/award_emoji? 4 | - snippet_award_emojis: award_emojis = GET projects/:project_id/merge_requests/:merge_request_id/award_emoji? 5 | 6 | - issue_award_emoji: award_emoji = GET projects/:project_id/issues/:issue_iid/award_emoji/:award_id 7 | - merge_request_award_emoji: award_emoji = GET projects/:project_id/merge_requests/:merge_request_iid/award_emoji/:award_id 8 | - snippet_award_emoji: award_emoji = GET projects/:project_id/snippets/:snippet_id/award_emoji/:award_id 9 | 10 | - create_issue_award_emoji: award_emoji = POST projects/:project_id/issues/:issue_iid/award_emoji? 11 | - create_merge_request_award_emoji: award_emoji = POST projects/:project_id/merge_requests/:merge_request_iid/award_emoji? 12 | - create_snippet_award_emoji: award_emoji = POST projects/:project_id/snippets/:snippet_id/award_emoji 13 | 14 | - delete_issue_award_emoji: award_emoji = DELETE projects/:project_id/issues/:issue_id/award_emoji/:award_id 15 | - delete_merge_request_award_emoji: award_emoji = DELETE projects/:project_id/merge_requests/:merge_request_id/award_emoji/:award_id 16 | - delete_snippet_award_emoji: award_emoji = DELETE projects/:project_id/snippets/:snippet_id/award_emoji/:award_id 17 | 18 | - issue_note_award_emojis: award_emojis = GET projects/:project_id/issues/:issue_iid/notes/:note_id/award_emoji 19 | - issue_note_award_emoji: award_emoji = GET projects/:project_id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id 20 | - create_issue_note_award_emoji: award_emoji = POST projects/:project_id/issues/:issue_iid/notes/:note_id/award_emoji? 21 | - delete_issue_note_award_emoji: award_emoji = DELETE projects/:project_id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id 22 | 23 | - merge_request_note_award_emojis: award_emojis = GET projects/:project_id/merge_requests/:merge_request_iid/notes/:note_id/award_emoji 24 | - merge_request_note_award_emoji: award_emoji = GET projects/:project_id/merge_requests/:merge_request_iid/notes/:note_id/award_emoji/:award_id 25 | - create_merge_request_note_award_emoji: award_emoji = POST projects/:project_id/merge_requests/:merge_request_iid/notes/:note_id/award_emoji? 26 | - delete_merge_request_note_award_emoji: award_emoji = DELETE projects/:project_id/merge_requests/:merge_request_iid/notes/:note_id/award_emoji/:award_id 27 | 28 | # What about snippets? Do we make snippet_note_award_emoji methods? The docs only 29 | # reference issues and merge requests. Its probably a failing in GitLab's regularly 30 | # inconsistent documentation. If they exist feel free to add them here and make a PR. 31 | -------------------------------------------------------------------------------- /author/sections/discussions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - issue_discussions: discussions = GET projects/:project_id/issues/:issue_iid/discussions? 3 | - issue_discussion: discussion = GET projects/:project_id/issues/:issue_iid/discussions/:discussion_id 4 | - create_issue_discussion: discussion = POST projects/:project_id/issues/:issue_iid/discussions? 5 | - create_issue_discussion_note: POST projects/:project_id/issues/:issue_iid/discussions/:discussion_id/notes? 6 | - edit_issue_discussion_note: PUT projects/:project_id/issues/:issue_iid/discussions/:discussion_id/notes/:note_id? 7 | - delete_issue_discussion_note: DELETE projects/:project_id/issues/:issue_iid/discussions/:discussion_id/notes/:note_id 8 | 9 | - project_snippet_discussions: discussions = GET projects/:project_id/snippets/:snippet_id/discussions? 10 | - project_snippet_discussion: discussion = GET projects/:project_id/snippets/:snippet_id/discussions/:discussion_id 11 | - create_project_snippet_discussion: discussion = POST projects/:project_id/snippets/:snippet_id/discussions? 12 | - create_project_snippet_discussion_note: POST projects/:project_id/snippets/:snippet_id/discussions/:discussion_id/notes? 13 | - edit_project_snippet_discussion_note: PUT projects/:project_id/snippets/:snippet_id/discussions/:discussion_id/notes/:note_id? 14 | - delete_project_snippet_discussion_note: DELETE projects/:project_id/snippets/:snippet_id/discussions/:discussion_id/notes/:note_id 15 | 16 | - merge_request_discussions: discussions = GET projects/:project_id/merge_requests/:merge_request_iid/discussions? 17 | - merge_request_discussion: discussion = GET projects/:project_id/merge_requests/:merge_request_iid/discussions/:discussion_id 18 | - create_merge_request_discussion: discussion = POST projects/:project_id/merge_requests/:merge_request_iid/discussions? 19 | - resolve_merge_request_discussion: PUT projects/:project_id/merge_requests/:merge_request_iid/discussions/:discussion_id? 20 | - create_merge_request_discussion_note: POST projects/:project_id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes? 21 | - edit_merge_request_discussion_note: PUT projects/:project_id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes/:note_id? 22 | - delete_merge_request_discussion_note: DELETE projects/:project_id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes/:note_id 23 | 24 | - commit_discussions: discussions = GET projects/:project_id/commits/:commit_id/discussions? 25 | - commit_discussion: discussion = GET projects/:project_id/commits/:commit_id/discussions/:discussion_id 26 | - create_commit_discussion: discussion = POST projects/:project_id/commits/:commit_id/discussions? 27 | - create_commit_discussion_note: POST projects/:project_id/commits/:commit_id/discussions/:discussion_id/notes? 28 | - edit_commit_discussion_note: PUT projects/:project_id/commits/:commit_id/discussions/:discussion_id/notes/:note_id? 29 | - delete_commit_discussion_note: DELETE projects/:project_id/commits/:commit_id/discussions/:discussion_id/notes/:note_id 30 | -------------------------------------------------------------------------------- /lib/GitLab/API/v4/Constants.pm: -------------------------------------------------------------------------------- 1 | package GitLab::API::v4::Constants; 2 | our $VERSION = '0.27'; 3 | 4 | =encoding utf8 5 | 6 | =head1 NAME 7 | 8 | GitLab::API::v4::Constants - GitLab API v4 constants. 9 | 10 | =head1 SYNOPSIS 11 | 12 | use GitLab::API::v4::Constants qw( :all ); 13 | 14 | print $GITLAB_ACCESS_LEVEL_GUEST; # 10 15 | 16 | =cut 17 | 18 | use Const::Fast; 19 | 20 | use strictures 2; 21 | use namespace::clean; 22 | 23 | use Exporter qw( import ); 24 | our @EXPORT_OK; 25 | our %EXPORT_TAGS = ( all => \@EXPORT_OK ); 26 | 27 | =head1 CONSTANTS 28 | 29 | =head2 $GITLAB_ACCESS_LEVEL_NO_ACCESS 30 | 31 | C<0> 32 | 33 | =cut 34 | 35 | const our $GITLAB_ACCESS_LEVEL_NO_ACCESS => 0; 36 | push @EXPORT_OK, '$GITLAB_ACCESS_LEVEL_NO_ACCESS'; 37 | 38 | =head2 $GITLAB_ACCESS_LEVEL_GUEST 39 | 40 | C<10> 41 | 42 | =cut 43 | 44 | const our $GITLAB_ACCESS_LEVEL_GUEST => 10; 45 | push @EXPORT_OK, '$GITLAB_ACCESS_LEVEL_GUEST'; 46 | 47 | =head2 $GITLAB_ACCESS_LEVEL_REPORTER 48 | 49 | C<20> 50 | 51 | =cut 52 | 53 | const our $GITLAB_ACCESS_LEVEL_REPORTER => 20; 54 | push @EXPORT_OK, '$GITLAB_ACCESS_LEVEL_REPORTER'; 55 | 56 | =head2 $GITLAB_ACCESS_LEVEL_DEVELOPER 57 | 58 | C<30> 59 | 60 | =cut 61 | 62 | const our $GITLAB_ACCESS_LEVEL_DEVELOPER => 30; 63 | push @EXPORT_OK, '$GITLAB_ACCESS_LEVEL_DEVELOPER'; 64 | 65 | =head2 $GITLAB_ACCESS_LEVEL_MASTER 66 | 67 | C<40> 68 | 69 | =cut 70 | 71 | const our $GITLAB_ACCESS_LEVEL_MASTER => 40; 72 | push @EXPORT_OK, '$GITLAB_ACCESS_LEVEL_MASTER'; 73 | 74 | =head2 $GITLAB_ACCESS_LEVEL_OWNER 75 | 76 | C<50> 77 | 78 | =cut 79 | 80 | const our $GITLAB_ACCESS_LEVEL_OWNER => 50; 81 | push @EXPORT_OK, '$GITLAB_ACCESS_LEVEL_OWNER'; 82 | 83 | =head2 @GITLAB_ACCESS_LEVELS 84 | 85 | An array containing the values for L, 86 | L, 87 | L, L, 88 | L, and L. 89 | 90 | =cut 91 | 92 | const our @GITLAB_ACCESS_LEVELS => ( 93 | $GITLAB_ACCESS_LEVEL_NO_ACCESS, 94 | $GITLAB_ACCESS_LEVEL_GUEST, 95 | $GITLAB_ACCESS_LEVEL_REPORTER, 96 | $GITLAB_ACCESS_LEVEL_DEVELOPER, 97 | $GITLAB_ACCESS_LEVEL_MASTER, 98 | $GITLAB_ACCESS_LEVEL_OWNER, 99 | ); 100 | push @EXPORT_OK, '@GITLAB_ACCESS_LEVELS'; 101 | 102 | =head2 $GITLAB_SNIPPET_VISIBILITY_LEVEL_PRIVATE 103 | 104 | C 105 | 106 | =cut 107 | 108 | const our $GITLAB_SNIPPET_VISIBILITY_LEVEL_PRIVATE => 'private'; 109 | push @EXPORT_OK, '$GITLAB_SNIPPET_VISIBILITY_LEVEL_PRIVATE'; 110 | 111 | =head2 $GITLAB_SNIPPET_VISIBILITY_LEVEL_INTERNAL 112 | 113 | C 114 | 115 | =cut 116 | 117 | const our $GITLAB_SNIPPET_VISIBILITY_LEVEL_INTERNAL => 'internal'; 118 | push @EXPORT_OK, '$GITLAB_SNIPPET_VISIBILITY_LEVEL_INTERNAL'; 119 | 120 | =head2 $GITLAB_SNIPPET_VISIBILITY_LEVEL_PUBLIC 121 | 122 | C 123 | 124 | =cut 125 | 126 | const our $GITLAB_SNIPPET_VISIBILITY_LEVEL_PUBLIC => 'public'; 127 | push @EXPORT_OK, '$GITLAB_SNIPPET_VISIBILITY_LEVEL_PUBLIC'; 128 | 129 | =head2 @GITLAB_SNIPPET_VISIBILITY_LEVELS 130 | 131 | An array containing the values for 132 | L, 133 | L, and 134 | L. 135 | 136 | =cut 137 | 138 | const our @GITLAB_SNIPPET_VISIBILITY_LEVELS => ( 139 | $GITLAB_SNIPPET_VISIBILITY_LEVEL_PRIVATE, 140 | $GITLAB_SNIPPET_VISIBILITY_LEVEL_INTERNAL, 141 | $GITLAB_SNIPPET_VISIBILITY_LEVEL_PUBLIC, 142 | ); 143 | push @EXPORT_OK, '@GITLAB_SNIPPET_VISIBILITY_LEVELS'; 144 | 145 | 1; 146 | __END__ 147 | 148 | =head1 SUPPORT 149 | 150 | See L. 151 | 152 | =head1 AUTHORS 153 | 154 | See L. 155 | 156 | =head1 LICENSE 157 | 158 | See L. 159 | 160 | =cut 161 | 162 | -------------------------------------------------------------------------------- /META.json: -------------------------------------------------------------------------------- 1 | { 2 | "abstract" : "A complete GitLab API v4 client.", 3 | "author" : [ 4 | "Aran Clary Deltac " 5 | ], 6 | "dynamic_config" : 0, 7 | "generated_by" : "Minilla/v3.1.21, CPAN::Meta::Converter version 2.150010", 8 | "license" : [ 9 | "perl_5" 10 | ], 11 | "meta-spec" : { 12 | "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", 13 | "version" : 2 14 | }, 15 | "name" : "GitLab-API-v4", 16 | "no_index" : { 17 | "directory" : [ 18 | "t", 19 | "xt", 20 | "inc", 21 | "share", 22 | "eg", 23 | "examples", 24 | "author", 25 | "builder" 26 | ] 27 | }, 28 | "prereqs" : { 29 | "configure" : { 30 | "requires" : { 31 | "Module::Build::Tiny" : "0.035" 32 | } 33 | }, 34 | "develop" : { 35 | "requires" : { 36 | "Test::CPAN::Meta" : "0", 37 | "Test::MinimumVersion::Fast" : "0.04", 38 | "Test::PAUSE::Permissions" : "0.07", 39 | "Test::Pod" : "1.41", 40 | "Test::Spellunker" : "v0.2.7" 41 | } 42 | }, 43 | "runtime" : { 44 | "requires" : { 45 | "Carp" : "0", 46 | "Const::Fast" : "0.014", 47 | "Exporter" : "0", 48 | "Getopt::Long" : "0", 49 | "HTTP::Tiny" : "0.059", 50 | "HTTP::Tiny::Multipart" : "0.05", 51 | "IO::Prompter" : "0.004014", 52 | "JSON::MaybeXS" : "1.003007", 53 | "List::Util" : "0", 54 | "Log::Any" : "1.703", 55 | "Log::Any::Adapter" : "1.703", 56 | "Log::Any::Adapter::Screen" : "0.13", 57 | "Moo" : "2.003000", 58 | "Path::Tiny" : "0.079", 59 | "Pod::Usage" : "0", 60 | "Try::Tiny" : "0.28", 61 | "Types::Common::Numeric" : "1.002001", 62 | "Types::Common::String" : "1.002001", 63 | "Types::Standard" : "1.002001", 64 | "URI" : "1.62", 65 | "URI::Escape" : "1.72", 66 | "namespace::clean" : "0.27", 67 | "perl" : "5.010001", 68 | "strictures" : "2.000003" 69 | } 70 | }, 71 | "test" : { 72 | "requires" : { 73 | "Log::Any::Adapter::TAP" : "0.003003", 74 | "MIME::Base64" : "3.15", 75 | "Test2::V0" : "0.000094" 76 | } 77 | } 78 | }, 79 | "release_status" : "unstable", 80 | "resources" : { 81 | "bugtracker" : { 82 | "web" : "https://github.com/bluefeet/GitLab-API-v4/issues" 83 | }, 84 | "homepage" : "https://github.com/bluefeet/GitLab-API-v4", 85 | "repository" : { 86 | "type" : "git", 87 | "url" : "https://github.com/bluefeet/GitLab-API-v4.git", 88 | "web" : "https://github.com/bluefeet/GitLab-API-v4" 89 | } 90 | }, 91 | "version" : "0.27", 92 | "x_authority" : "cpan:BLUEFEET", 93 | "x_contributors" : [ 94 | "Dan Book ", 95 | "Dave Webb ", 96 | "Dmitry Frolov ", 97 | "Dotan Dimet ", 98 | "Graham Knop ", 99 | "James Wright ", 100 | "Jonathan Taylor ", 101 | "José Joaquín Atria ", 102 | "Kieren Diment ", 103 | "Luc Didry ", 104 | "Marek R. Sotola ", 105 | "Nigel Gregoire ", 106 | "Simon Ruderich ", 107 | "Stig Palmquist ", 108 | "Thomas Klausner ", 109 | "g0t mi1k ", 110 | "gregor herrmann ", 111 | "royce55 ", 112 | "trunov-ms " 113 | ], 114 | "x_serialization_backend" : "JSON::PP version 4.07", 115 | "x_static_install" : 1 116 | } 117 | -------------------------------------------------------------------------------- /lib/GitLab/API/v4/Mock/RESTClient.pm: -------------------------------------------------------------------------------- 1 | package GitLab::API::v4::Mock::RESTClient; 2 | our $VERSION = '0.27'; 3 | 4 | =encoding utf8 5 | 6 | =head1 NAME 7 | 8 | GitLab::API::v4::Mock::RESTClient - Mocked REST client that doesn't actually make HTTP requests. 9 | 10 | =head1 DESCRIPTION 11 | 12 | This module is a subclass of L. It 13 | modifies it to divert HTTP requests to 14 | L rather than making live requests. 15 | 16 | This module is used by L. 17 | 18 | =cut 19 | 20 | use GitLab::API::v4::Mock::Engine; 21 | use JSON::MaybeXS; 22 | use URI; 23 | 24 | use Moo; 25 | use strictures 2; 26 | use namespace::clean; 27 | 28 | extends 'GitLab::API::v4::RESTClient'; 29 | 30 | my @ENDPOINTS; 31 | 32 | sub has_endpoint { 33 | my ($method, $path_re, $sub) = @_; 34 | 35 | push @ENDPOINTS, [ 36 | $method, $path_re, $sub, 37 | ]; 38 | 39 | return; 40 | } 41 | 42 | sub _http_tiny_request { 43 | my ($self, $req_method, $req) = @_; 44 | 45 | die "req_method may only be 'request' at this time" 46 | if $req_method ne 'request'; 47 | 48 | my ($http_method, $url, $options) = @$req; 49 | 50 | my $path = URI->new( $url )->path(); 51 | $path =~ s{^.*api/v4/}{}; 52 | 53 | foreach my $endpoint (@ENDPOINTS) { 54 | my ($endpoint_method, $path_re, $sub) = @$endpoint; 55 | 56 | next if $endpoint_method ne $http_method; 57 | next if $path !~ $path_re; 58 | 59 | my @captures = ($path =~ $path_re); 60 | 61 | my ($status, $content) = $sub->( 62 | $self, 63 | [$http_method, $url, $options], 64 | @captures, 65 | ); 66 | 67 | $content = encode_json( $content ) if ref $content; 68 | 69 | return { 70 | status => $status, 71 | success => ($status =~ m{^2\d\d$}) ? 1 : 0, 72 | defined( $content ) ? (content=>$content) : (), 73 | }; 74 | } 75 | 76 | die "No endpoint matched the $http_method '$path' endpoint"; 77 | } 78 | 79 | =head1 ATTRIBUTES 80 | 81 | =head2 engine 82 | 83 | The L used behind the hood. 84 | 85 | =cut 86 | 87 | has engine => ( 88 | is => 'lazy', 89 | init_arg => undef, 90 | ); 91 | sub _build_engine { 92 | return GitLab::API::v4::Mock::Engine->new(); 93 | } 94 | 95 | =head1 USER ENDPOINTS 96 | 97 | =head2 GET users 98 | 99 | Handles L. 100 | 101 | =cut 102 | 103 | has_endpoint GET => qr{^users$}, sub{ 104 | my ($self) = @_; 105 | return 200, $self->engine->users(); 106 | }; 107 | 108 | =head2 GET user/:id 109 | 110 | Handles L. 111 | 112 | =cut 113 | 114 | has_endpoint GET => qr{^users/(\d+)$}, sub{ 115 | my ($self, $req, $id) = @_; 116 | 117 | my $user = $self->engine->user( $id ); 118 | return 404 if !$user; 119 | 120 | return 200, $user; 121 | }; 122 | 123 | =head2 POST users 124 | 125 | Handles L. 126 | 127 | =cut 128 | 129 | has_endpoint POST => qr{^users$}, sub{ 130 | my ($self, $req) = @_; 131 | 132 | my $user = decode_json( $req->[2]->{content} ); 133 | $self->engine->create_user( $user ); 134 | 135 | return 204; 136 | }; 137 | 138 | =head2 PUT user/:id 139 | 140 | Handles L. 141 | 142 | =cut 143 | 144 | has_endpoint PUT => qr{^users/(\d+)$}, sub{ 145 | my ($self, $req, $id) = @_; 146 | 147 | my $data = decode_json( $req->[2]->{content} ); 148 | 149 | my $user = $self->engine->update_user( $id, $data ); 150 | return 404 if !$user; 151 | 152 | return 204; 153 | }; 154 | 155 | =head2 DELETE user/:id 156 | 157 | Handles L. 158 | 159 | =cut 160 | 161 | has_endpoint DELETE => qr{^users/(\d+)$}, sub{ 162 | my ($self, $req, $id) = @_; 163 | 164 | my $user = $self->engine->delete_user( $id ); 165 | return 404 if !$user; 166 | 167 | return 204; 168 | }; 169 | 170 | 1; 171 | __END__ 172 | 173 | =head1 SUPPORT 174 | 175 | See L. 176 | 177 | =head1 AUTHORS 178 | 179 | See L. 180 | 181 | =head1 LICENSE 182 | 183 | See L. 184 | 185 | =cut 186 | 187 | -------------------------------------------------------------------------------- /lib/GitLab/API/v4/Mock/Engine.pm: -------------------------------------------------------------------------------- 1 | package GitLab::API::v4::Mock::Engine; 2 | our $VERSION = '0.27'; 3 | 4 | =encoding utf8 5 | 6 | =head1 NAME 7 | 8 | GitLab::API::v4::Mock::Engine - Mocking the internals of a GitLab server. 9 | 10 | =head1 SYNOPSIS 11 | 12 | use GitLab::API::v4::Mock::Engine; 13 | 14 | my $engine = GitLab::API::v4::Mock::Engine->new(); 15 | 16 | my $user = $engine->create_user({ 17 | email => $email, 18 | username => $username, 19 | name => $name, 20 | ..., 21 | }); 22 | 23 | print "User created with ID: $user->{id}\n"; 24 | 25 | =head1 DESCRIPTION 26 | 27 | This module provides the tooling to run a mock of the internal state 28 | of a GitLab server. 29 | 30 | At this time very little is validated. For example, when you 31 | create a user with L there is no logic which double 32 | checks that you specify the required C field or that you 33 | don't put in fields that are unexpected. 34 | 35 | =cut 36 | 37 | use Moo; 38 | use strictures 2; 39 | use namespace::clean; 40 | 41 | =head1 ATTRIBUTES 42 | 43 | =head2 next_ids 44 | 45 | my $ids = $engine->next_ids(); 46 | 47 | A hash reference containing object types to the next ID. 48 | 49 | Used by L. 50 | 51 | =cut 52 | 53 | has next_ids => ( 54 | is => 'ro', 55 | init_arg => undef, 56 | default => sub{ {} }, 57 | ); 58 | 59 | =head2 users 60 | 61 | my $users = $engine->users(); 62 | foreach my $user (@$users) { ... } 63 | 64 | Returns the full array reference of all users hash references. 65 | 66 | =cut 67 | 68 | has users => ( 69 | is => 'ro', 70 | init_arg => undef, 71 | default => sub{ [] }, 72 | ); 73 | 74 | =head1 METHODS 75 | 76 | =head2 next_id_for 77 | 78 | my $id = $engine->next_id_for( 'user' ); 79 | 80 | Given an object type this will return the next unused ID. 81 | 82 | =cut 83 | 84 | sub next_id_for { 85 | my ($self, $for) = @_; 86 | 87 | my $next_id = $self->next_ids->{$for} || 1; 88 | $self->next_ids->{$for} = $next_id + 1; 89 | 90 | return $next_id; 91 | } 92 | 93 | =head1 USER METHODS 94 | 95 | =head2 user 96 | 97 | my $user = $engine->user( $id ); 98 | 99 | Returns a user hash reference for the given ID. 100 | 101 | If no user is found with the ID then C is returned. 102 | 103 | =cut 104 | 105 | sub user { 106 | my ($self, $id) = @_; 107 | 108 | foreach my $user (@{ $self->users() }) { 109 | return $user if $user->{id} == $id; 110 | } 111 | 112 | return undef; 113 | } 114 | 115 | =head2 create_user 116 | 117 | my $user = $engine->create_user( $user ); 118 | my $id = $user->{id}; 119 | 120 | Takes a user hash reference, sets the C field, and stores it in 121 | L. 122 | 123 | Returns the updated user hash reference. 124 | 125 | =cut 126 | 127 | sub create_user { 128 | my ($self, $user) = @_; 129 | 130 | $user->{id} = $self->next_id_for( 'user' ); 131 | push @{ $self->users() }, $user; 132 | 133 | return $user; 134 | } 135 | 136 | =head2 update_user 137 | 138 | my $user = $engine->update_user( $id, $new_user_data ); 139 | 140 | Takes the ID of the user to update and a hash reference of fields 141 | to update. 142 | 143 | Returns the updated user hash reference. 144 | 145 | =cut 146 | 147 | sub update_user { 148 | my ($self, $id, $data) = @_; 149 | 150 | my $user = $self->user( $id ); 151 | return undef if !$user; 152 | 153 | %$user = ( 154 | %$user, 155 | %$data, 156 | ); 157 | 158 | return $user; 159 | } 160 | 161 | =head2 delete_user 162 | 163 | my $user = $engine->delete_user( $id ); 164 | 165 | Deletes the user with the specified ID from L. 166 | 167 | If no user is found with the ID then C is returned. 168 | Otherwise the user hash reference is returned. 169 | 170 | =cut 171 | 172 | sub delete_user { 173 | my ($self, $id) = @_; 174 | 175 | my $users = $self->users(); 176 | 177 | my @new; 178 | my $found_user; 179 | foreach my $user (@$users) { 180 | if ($user->{id} == $id and !$found_user) { 181 | $found_user = $user; 182 | next; 183 | } 184 | push @new, $user; 185 | } 186 | 187 | return undef if !$found_user; 188 | 189 | @$users = @new; 190 | 191 | return $found_user; 192 | } 193 | 194 | 1; 195 | __END__ 196 | 197 | =head1 SUPPORT 198 | 199 | See L. 200 | 201 | =head1 AUTHORS 202 | 203 | See L. 204 | 205 | =head1 LICENSE 206 | 207 | See L. 208 | 209 | =cut 210 | 211 | -------------------------------------------------------------------------------- /author/generate.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strictures 1; 3 | 4 | use YAML::XS qw(); 5 | use Data::Dumper; 6 | use Path::Tiny; 7 | 8 | my $dir = path('sections'); 9 | my $header = path('header.pm')->slurp(); 10 | my $footer = path('footer.pm')->slurp(); 11 | my $config = YAML::XS::Load( path('config.yml')->slurp() ); 12 | 13 | print $header; 14 | 15 | print "=head1 API METHODS\n\n"; 16 | 17 | foreach my $section_pack (@{ $config->{sections} }) { 18 | foreach my $section_name (keys %$section_pack) { 19 | my $section = $section_pack->{$section_name}; 20 | 21 | my $file = $dir->child("$section_name.yml"); 22 | my $endpoints = YAML::XS::Load( $file->slurp() ); 23 | 24 | print "=head2 $section->{head}\n\n"; 25 | print "See L<$section->{doc_url}>.\n\n"; 26 | print "=over\n\n"; 27 | 28 | foreach my $endpoint (@$endpoints) { 29 | if (keys(%$endpoint) == 1) { 30 | my ($method) = keys %$endpoint; 31 | $endpoint = { 32 | method => $method, 33 | spec => $endpoint->{$method}, 34 | }; 35 | } 36 | 37 | my $method = $endpoint->{method}; 38 | my $spec = $endpoint->{spec}; 39 | 40 | my ($return, $verb, $path, $params_ok); 41 | if ($spec =~ m{^(?:(\S+) = |)(GET|POST|PUT|DELETE) ([^/\s]\S*?[^/\s]?)(\??)$}) { 42 | ($return, $verb, $path, $params_ok) = ($1, $2, $3, $4); 43 | } 44 | else { 45 | die "Invalid spec ($method): $spec"; 46 | } 47 | 48 | my $no_decode = 0; 49 | $no_decode = 1 if !$return; 50 | $no_decode = 1 if $endpoint->{no_decode}; 51 | 52 | print "=item $method\n\n"; 53 | print ' '; 54 | 55 | print "my \$$return = " if $return; 56 | 57 | print "\$api->$method("; 58 | 59 | my @args = ( 60 | map { ($_ =~ m{^:(.+)$}) ? "\$$1" : () } 61 | split(m{/}, $path) 62 | ); 63 | 64 | push @args, '\%params' if $params_ok; 65 | 66 | if (@args) { 67 | print "\n" . join('', 68 | map { " $_,\n" } 69 | @args 70 | ); 71 | print ' '; 72 | } 73 | 74 | print ");\n\n"; 75 | 76 | print "Sends a C<$verb> request to C<$path>"; 77 | print ' and returns the ' . ($no_decode ? 'raw' : 'decoded') . ' response content' if $return; 78 | print ".\n\n"; 79 | print "$endpoint->{note}\n" if $endpoint->{note}; 80 | print "=cut\n\n"; 81 | 82 | print "sub $method {\n"; 83 | print " my \$self = shift;\n"; 84 | 85 | if (@args) { 86 | my $min_args = @args; 87 | my $max_args = @args; 88 | $min_args-- if $params_ok; 89 | 90 | if ($min_args == $max_args) { 91 | print " croak '$method must be called with $min_args arguments' if \@_ != $min_args;\n"; 92 | } 93 | else { 94 | print " croak '$method must be called with $min_args to $max_args arguments' if \@_ < $min_args or \@_ > $max_args;\n"; 95 | } 96 | 97 | my $i = 0; 98 | foreach my $arg (@args) { 99 | my $is_params = ($params_ok and $i==$#args) ? 1 : 0; 100 | if ($is_params) { 101 | print " croak 'The last argument ($arg) to $method must be a hash ref' if defined(\$_[$i]) and ref(\$_[$i]) ne 'HASH';\n"; 102 | } 103 | else { 104 | my $number = $i + 1; 105 | print " croak 'The #$number argument ($arg) to $method must be a scalar' if ref(\$_[$i]) or (!defined \$_[$i]);\n"; 106 | } 107 | $i ++; 108 | } 109 | 110 | print " my \$params = (\@_ == $max_args) ? pop() : undef;\n" if $params_ok; 111 | } 112 | else { 113 | print " croak \"The $method method does not take any arguments\" if \@_;\n"; 114 | } 115 | 116 | print " my \$options = {};\n"; 117 | print " \$options->{decode} = 0;\n" if $no_decode; 118 | 119 | if ($params_ok) { 120 | my $params_key = ($verb eq 'GET' or $verb eq 'HEAD') ? 'query' : 'content'; 121 | print " \$options->{$params_key} = \$params if defined \$params;\n"; 122 | } 123 | 124 | print ' '; 125 | print 'return ' if $return; 126 | print "\$self->_call_rest_client( '$verb', '$path', [\@_], \$options );\n"; 127 | print " return;\n" if !$return; 128 | print "}\n\n"; 129 | } 130 | 131 | print "=back\n\n"; 132 | }} 133 | 134 | print "=cut\n\n"; 135 | 136 | print $footer; 137 | -------------------------------------------------------------------------------- /lib/GitLab/API/v4/Paginator.pm: -------------------------------------------------------------------------------- 1 | package GitLab::API::v4::Paginator; 2 | our $VERSION = '0.27'; 3 | 4 | =encoding utf8 5 | 6 | =head1 NAME 7 | 8 | GitLab::API::v4::Paginator - Iterate through paginated GitLab v4 API records. 9 | 10 | =head1 DESCRIPTION 11 | 12 | There should be no need to create objects of this type 13 | directly, instead use L which 14 | simplifies things a bit. 15 | 16 | =cut 17 | 18 | use Carp qw( croak ); 19 | use Types::Common::String -types; 20 | use Types::Standard -types; 21 | 22 | use Moo; 23 | use strictures 2; 24 | use namespace::clean; 25 | 26 | =head1 REQUIRED ARGUMENTS 27 | 28 | =head2 method 29 | 30 | The name of the method subroutine to call on the L object 31 | to get records from. 32 | 33 | This method must accept a hash ref of parameters as the last 34 | argument, adhere to the C and C parameters, and 35 | return an array ref. 36 | 37 | =cut 38 | 39 | has method => ( 40 | is => 'ro', 41 | isa => NonEmptySimpleStr, 42 | required => 1, 43 | ); 44 | 45 | =head2 api 46 | 47 | The L object. 48 | 49 | =cut 50 | 51 | has api => ( 52 | is => 'ro', 53 | isa => InstanceOf[ 'GitLab::API::v4' ], 54 | required => 1, 55 | ); 56 | 57 | =head1 OPTIONAL ARGUMENTS 58 | 59 | =head2 args 60 | 61 | The arguments to use when calling the L, the same arguments 62 | you would use when you call the method yourself on the L 63 | object, minus the C<\%params> hash ref. 64 | 65 | =cut 66 | 67 | has args => ( 68 | is => 'ro', 69 | isa => ArrayRef, 70 | default => sub{ [] }, 71 | ); 72 | 73 | =head2 params 74 | 75 | The C<\%params> hash ref argument. 76 | 77 | =cut 78 | 79 | has params => ( 80 | is => 'ro', 81 | isa => HashRef, 82 | default => sub{ {} }, 83 | ); 84 | 85 | =head1 METHODS 86 | 87 | =cut 88 | 89 | has _records => ( 90 | is => 'rw', 91 | init_arg => undef, 92 | default => sub{ [] }, 93 | ); 94 | 95 | has _page => ( 96 | is => 'rw', 97 | init_arg => undef, 98 | default => 0, 99 | ); 100 | 101 | has _last_page => ( 102 | is => 'rw', 103 | init_arg => undef, 104 | default => 0, 105 | ); 106 | 107 | =head2 next_page 108 | 109 | while (my $records = $paginator->next_page()) { ... } 110 | 111 | Returns an array ref of records for the next page. 112 | 113 | =cut 114 | 115 | sub next_page { 116 | my ($self) = @_; 117 | 118 | return if $self->_last_page(); 119 | 120 | my $page = $self->_page() + 1; 121 | my $params = $self->params(); 122 | my $per_page = $params->{per_page} || 20; 123 | 124 | $params = { 125 | %$params, 126 | page => $page, 127 | per_page => $per_page, 128 | }; 129 | 130 | my $method = $self->method(); 131 | my $records = $self->api->$method( 132 | @{ $self->args() }, 133 | $params, 134 | ); 135 | 136 | croak("The $method method returned a non array ref value") 137 | if ref($records) ne 'ARRAY'; 138 | 139 | $self->_page( $page ); 140 | $self->_last_page( 1 ) if @$records < $per_page; 141 | $self->_records( [ @$records ] ); 142 | 143 | return if !@$records; 144 | 145 | return $records; 146 | } 147 | 148 | =head2 next 149 | 150 | while (my $record = $paginator->next()) { ... } 151 | 152 | Returns the next record in the current page. If all records have 153 | been exhausted then L will automatically be called. 154 | This way if you want to ignore pagination you can just call C 155 | over and over again to walk through all the records. 156 | 157 | =cut 158 | 159 | sub next { 160 | my ($self) = @_; 161 | 162 | my $records = $self->_records(); 163 | return shift(@$records) if @$records; 164 | 165 | return if $self->_last_page(); 166 | 167 | $self->next_page(); 168 | 169 | $records = $self->_records(); 170 | return shift(@$records) if @$records; 171 | 172 | return; 173 | } 174 | 175 | =head2 all 176 | 177 | my $records = $paginator->all(); 178 | 179 | This is just an alias for calling L over and over 180 | again to build an array ref of all records. 181 | 182 | =cut 183 | 184 | sub all { 185 | my ($self) = @_; 186 | 187 | $self->reset(); 188 | 189 | my @records; 190 | while (my $page = $self->next_page()) { 191 | push @records, @$page; 192 | } 193 | 194 | return \@records; 195 | } 196 | 197 | =head2 reset 198 | 199 | $paginator->reset(); 200 | 201 | Reset the paginator back to its original state on the first page 202 | with no records retrieved yet. 203 | 204 | =cut 205 | 206 | sub reset { 207 | my ($self) = @_; 208 | $self->_records( [] ); 209 | $self->_page( 0 ); 210 | $self->_last_page( 0 ); 211 | return; 212 | } 213 | 214 | 1; 215 | __END__ 216 | 217 | =head1 SUPPORT 218 | 219 | See L. 220 | 221 | =head1 AUTHORS 222 | 223 | See L. 224 | 225 | =head1 LICENSE 226 | 227 | See L. 228 | 229 | =cut 230 | 231 | -------------------------------------------------------------------------------- /lib/GitLab/API/v4/WWWClient.pm: -------------------------------------------------------------------------------- 1 | package GitLab::API::v4::WWWClient; 2 | our $VERSION = '0.27'; 3 | 4 | =encoding utf8 5 | 6 | =head1 NAME 7 | 8 | GitLab::API::v4::WWWClient - A client that works against the GitLab web site. 9 | 10 | =head1 SYNOPSIS 11 | 12 | use GitLab::API::v4::WWWClient; 13 | 14 | my $client = GitLab::API::v4::WWWClient->new( 15 | url => 'https://git.example.com/', 16 | ); 17 | 18 | $client->sign_in( $username, $password ); 19 | 20 | my $res = $client->get( $path ); 21 | 22 | =head1 DESCRIPTION 23 | 24 | This class makes it possible to interact with the GitLab web site. 25 | 26 | =cut 27 | 28 | use Carp qw( croak ); 29 | use HTTP::Tiny; 30 | use List::Util qw( first ); 31 | use Types::Common::String qw( NonEmptySimpleStr ); 32 | 33 | use Moo; 34 | use strictures 2; 35 | use namespace::clean; 36 | 37 | has _session => ( 38 | is => 'rw', 39 | init_arg => undef, 40 | ); 41 | 42 | sub _croak_res { 43 | my ($verb, $url, $res) = @_; 44 | 45 | return if $res->{status} !~ m{^5}; 46 | 47 | local $Carp::Internal{ 'GitLab::API::v4::WWWClient' } = 1; 48 | 49 | croak sprintf( 50 | 'Error %sing %s (HTTP %s): %s', 51 | uc($verb), $url, 52 | $res->{status}, ($res->{reason} || 'Unknown'), 53 | ); 54 | } 55 | 56 | =head1 REQUIRED ARGUMENTS 57 | 58 | =head2 url 59 | 60 | This is the base URL to your GitLab web site. 61 | 62 | =cut 63 | 64 | has url => ( 65 | is => 'ro', 66 | isa => NonEmptySimpleStr, 67 | required => 1, 68 | ); 69 | 70 | =head1 METHODS 71 | 72 | =head2 sign_in 73 | 74 | $client->sign_in( $username, $password ); 75 | 76 | Signs in the client given the username and password. 77 | 78 | =cut 79 | 80 | sub sign_in { 81 | my ($self, $username, $password) = @_; 82 | 83 | my $tiny = HTTP::Tiny->new( 84 | verify_SSL => 1, 85 | max_redirect => 0, 86 | ); 87 | 88 | my $base_url = $self->url(); 89 | $base_url =~ s{/$}{}; 90 | my $sign_in_url = "$base_url/users/sign_in"; 91 | 92 | my $load_res = $tiny->get( $sign_in_url ); 93 | 94 | _croak_res( 'get', $sign_in_url, $load_res ); 95 | 96 | my $token = ( 97 | $load_res->{content} =~ 98 | m{name="authenticity_token" value="(.+?)"} 99 | )[0]; 100 | 101 | 102 | my ($first_session) = do{ 103 | my $set_cookie_headers = $load_res->{headers}->{ 'set-cookie' }; 104 | $set_cookie_headers = [ $set_cookie_headers ] if !ref $set_cookie_headers; 105 | my $value = first { $_ =~ m{^_gitlab_session=(.*)} } @$set_cookie_headers; 106 | $value =~ s{^_gitlab_session=}{}r; 107 | }; 108 | 109 | my $submit_res = $tiny->post_form( 110 | $sign_in_url, 111 | { 112 | 'utf8' => '✓', 113 | 'authenticity_token' => $token, 114 | 'user[login]' => $username, 115 | 'user[password]' => $password, 116 | 'user[remember_me]' => 0, 117 | }, 118 | { 119 | headers => { 120 | 'Referer' => $sign_in_url, 121 | 'Cookie' => "_gitlab_session=$first_session", 122 | 'Cookie2' => '$Version="1"', 123 | }, 124 | }, 125 | ); 126 | 127 | _croak_res( 'post', $sign_in_url, $submit_res ); 128 | 129 | my ($second_session) = do{ 130 | my $set_cookie_headers = $submit_res->{headers}->{ 'set-cookie' }; 131 | $set_cookie_headers = [ $set_cookie_headers ] if !ref $set_cookie_headers; 132 | my $value = first { $_ =~ m{^_gitlab_session=(.*)} } @$set_cookie_headers; 133 | $value =~ s{^_gitlab_session=}{}r; 134 | }; 135 | 136 | my $home_res = $tiny->get( 137 | $base_url, 138 | { 139 | headers => { 140 | 'Referer' => $sign_in_url, 141 | 'Cookie' => "_gitlab_session=$second_session", 142 | 'Cookie2' => '$Version="1"', 143 | }, 144 | }, 145 | ); 146 | 147 | _croak_res( 'get', $base_url, $home_res ); 148 | 149 | my $ok = ( $home_res->{content} =~ m{sign-out-link} ) ? 1 : 0; 150 | croak 'Failed to sign in' if !$ok; 151 | 152 | $self->_session( $second_session ); 153 | 154 | return; 155 | } 156 | 157 | =head2 get 158 | 159 | my $res = $client->get( $path ); 160 | 161 | Gets the path and returns the L response hash. 162 | 163 | =cut 164 | 165 | sub get { 166 | my ($self, $path) = @_; 167 | 168 | my $tiny = HTTP::Tiny->new( 169 | verify_SSL => 1, 170 | max_redirect => 0, 171 | ); 172 | 173 | my $base_url = $self->url(); 174 | $base_url =~ s{/$}{}; 175 | $path =~ s{^/}{}; 176 | my $url = "$base_url/$path"; 177 | 178 | my $session = $self->_session(); 179 | my $headers = $session ? { 180 | 'Cookie' => "_gitlab_session=$session", 181 | 'Cookie2' => '$Version="1"', 182 | } : {}; 183 | 184 | my $res = $tiny->get( 185 | $url, 186 | { headers=>$headers }, 187 | ); 188 | 189 | _croak_res( 'get', $url, $res ); 190 | 191 | return $res; 192 | } 193 | 194 | 1; 195 | __END__ 196 | 197 | =head1 SUPPORT 198 | 199 | See L. 200 | 201 | =head1 AUTHORS 202 | 203 | See L. 204 | 205 | =head1 LICENSE 206 | 207 | See L. 208 | 209 | =cut 210 | 211 | -------------------------------------------------------------------------------- /script/gitlab-api-v4: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strictures 2; 3 | 4 | use GitLab::API::v4::Config; 5 | use GitLab::API::v4::Constants qw( :all ); 6 | use GitLab::API::v4; 7 | use JSON::MaybeXS; 8 | use Log::Any qw( $log ); 9 | use Log::Any::Adapter::Screen; 10 | use Log::Any::Adapter; 11 | use Pod::Usage qw( pod2usage ); 12 | use Try::Tiny; 13 | 14 | if (!@ARGV) { 15 | print "USAGE: gitlab-api-v4 help\n"; 16 | exit 0; 17 | } 18 | 19 | my $config = GitLab::API::v4::Config->new(); 20 | 21 | $config->get_options( 22 | 'a|all' => \my $all, 23 | 'p|pretty' => \my $pretty, 24 | 'c|canonical' => \my $canonical, 25 | 26 | 'h|help' => \my $help, 27 | 'v|verbose' => \my $verbose, 28 | 'q|quiet' => \my $quiet, 29 | ); 30 | 31 | if ($help or (@ARGV and $ARGV[0] eq 'help')) { 32 | pod2usage( -verbose => 2 ); 33 | exit 0; 34 | } 35 | 36 | if (@ARGV and $ARGV[0] eq 'configure') { 37 | $config->configure(); 38 | exit 0; 39 | } 40 | 41 | my $min_level = 'info'; 42 | $min_level = 'trace' if $verbose; 43 | $min_level = 'error' if $quiet; 44 | 45 | Log::Any::Adapter->set( 46 | 'Screen', 47 | min_level => $min_level, 48 | stderr => 1, 49 | ); 50 | 51 | my $method = shift @ARGV; 52 | die "ERROR: No API method specified.\n" if !$method; 53 | my $orig_method = $method; 54 | $method =~ s{-}{_}g; 55 | 56 | die "ERROR: Unknown API method \"$orig_method\".\n" 57 | if !GitLab::API::v4->can( $method ); 58 | 59 | my @args; 60 | while (@ARGV and $ARGV[0] !~ m{:}) { 61 | my $arg = shift @ARGV; 62 | next if $arg eq '--'; 63 | 64 | push @args, $arg; 65 | } 66 | 67 | my $aliases = { 68 | access_level => { 69 | guest => $GITLAB_ACCESS_LEVEL_GUEST, 70 | reporter => $GITLAB_ACCESS_LEVEL_REPORTER, 71 | developer => $GITLAB_ACCESS_LEVEL_DEVELOPER, 72 | master => $GITLAB_ACCESS_LEVEL_MASTER, 73 | owner => $GITLAB_ACCESS_LEVEL_OWNER, 74 | }, 75 | }; 76 | 77 | my $params = {}; 78 | while (@ARGV) { 79 | my $arg = shift @ARGV; 80 | next if $arg eq '--'; 81 | 82 | if ($arg =~ m{^([^:]+):(.*)$}s) { 83 | my ($key, $value) = ($1, $2); 84 | 85 | $key =~ s{-}{_}g; 86 | 87 | if ($aliases->{$key} and exists $aliases->{$key}->{$value}) { 88 | $value = $aliases->{$key}->{$value}; 89 | } 90 | 91 | $params->{$key} = $value; 92 | } 93 | else { 94 | die "ERROR: Invalid API parameter \"$arg\".\n"; 95 | } 96 | } 97 | 98 | # Make sure we don't leak tokens in the logs. 99 | my $debug_config = { %{ $config->args() } }; 100 | $debug_config->{private_token} = 'xxxx' if $debug_config->{private_token}; 101 | $debug_config->{access_token} = 'xxxx' if $debug_config->{access_token}; 102 | 103 | $log->debug('config: ' . encode_json($debug_config)); 104 | $log->debug("method: $method"); 105 | $log->debug('arguments: ' . encode_json(\@args)); 106 | $log->debug('params: ' . encode_json($params)); 107 | 108 | my $api = GitLab::API::v4->new( $config->args() ); 109 | 110 | if ($all) { 111 | unshift @args, $method; 112 | $method = 'paginator'; 113 | } 114 | 115 | my $data = $api->$method( 116 | @args, 117 | %$params ? $params : (), 118 | ); 119 | 120 | $data = $data->all() if $all; 121 | 122 | binmode STDOUT, ':utf8'; 123 | my $json = JSON::MaybeXS->new(allow_nonref => 1); 124 | $json->pretty() if $pretty; 125 | $json->canonical() if $canonical; 126 | print $json->encode( $data ); 127 | 128 | __END__ 129 | 130 | =encoding utf8 131 | 132 | =head1 NAME 133 | 134 | gitlab-api-v4 - Command line interface to the GitLab API v4. 135 | 136 | =head1 SYNOPSIS 137 | 138 | # Generally: 139 | gitlab-api-v4 [] [ ...] [: ...] 140 | 141 | # List all groups: 142 | gitlab-api-v4 groups 143 | 144 | # List information about a project: 145 | gitlab-api-v4 project 146 | 147 | # Create an admin user: 148 | gitlab-api-v4 create-user \ 149 | username:foo \ 150 | password:xxxxxxxx \ 151 | email:user@example.com \ 152 | "name:Foo Smith" \ 153 | admin:1 154 | 155 | =head1 CONFIGURING 156 | 157 | You may configure this module with environment variables, command line options, 158 | and a configuration file. To setup the configuration file run: 159 | 160 | gitlab-api-v4 configure 161 | 162 | This will ask several interactive questions to help you configure this script. 163 | The information, which may include GitLab authentication tokens, is stored in 164 | C<~/.gitlab-api-v4.json>. 165 | 166 | Read more at L. 167 | 168 | =head1 OPTIONS 169 | 170 | =head2 url 171 | 172 | --url= 173 | 174 | Sets L. 175 | 176 | =head2 access-token 177 | 178 | --access-token= 179 | 180 | Sets L. 181 | 182 | =head2 private-token 183 | 184 | --private-token= 185 | 186 | Sets L. 187 | 188 | =head2 retries 189 | 190 | --retries= 191 | 192 | Sets L. 193 | 194 | =head2 all 195 | 196 | --all 197 | -a 198 | 199 | Retrieves all results when the results would normally be paged. 200 | See L for details. 201 | 202 | =head2 pretty 203 | 204 | --pretty 205 | -p 206 | 207 | Enables the L feature. 208 | 209 | =head2 canonical 210 | 211 | --canonical 212 | -c 213 | 214 | Enables the L feature. 215 | 216 | =head1 API METHOD 217 | 218 | 219 | 220 | The API method to call - one of the methods documented in 221 | L. 222 | 223 | =head1 API ARGUMENTS 224 | 225 | ... 226 | 227 | Any arguments that the L requires. 228 | 229 | =head1 API PARAMETERS 230 | 231 | : ... 232 | 233 | Any parameters that the L accepts. 234 | 235 | =head2 access-level 236 | 237 | access-level:guest 238 | access-level:reporter 239 | access-level:developer 240 | access-level:master 241 | access-level:owner 242 | 243 | There are mappings setup for the various C parameters 244 | so that you can, for example, specify C and it 245 | will be automatically converted to C. 246 | 247 | =head1 SUPPORT 248 | 249 | See L. 250 | 251 | =head1 AUTHORS 252 | 253 | See L. 254 | 255 | =head1 COPYRIGHT AND LICENSE 256 | 257 | See L. 258 | 259 | =cut 260 | 261 | -------------------------------------------------------------------------------- /lib/GitLab/API/v4/Config.pm: -------------------------------------------------------------------------------- 1 | package GitLab::API::v4::Config; 2 | our $VERSION = '0.27'; 3 | 4 | =encoding utf8 5 | 6 | =head1 NAME 7 | 8 | GitLab::API::v4::Config - Load configuration from a file, environment, 9 | and/or CLI options. 10 | 11 | =head1 SYNOPSIS 12 | 13 | use GitLab::API::v4; 14 | use GitLab::API::v4::Config; 15 | 16 | my $config = GitLab::API::v4::Config->new(); 17 | my $api = GitLab::API::v4->new( $config->args() ); 18 | 19 | =head1 DESCRIPTION 20 | 21 | This module is used by L to load configuration. 22 | 23 | If you are using L directly then this module will not be 24 | automatically used, but you are welcome to explicitly use it as shown in the 25 | L. 26 | 27 | =cut 28 | 29 | use Getopt::Long; 30 | use IO::Prompter; 31 | use JSON::MaybeXS; 32 | use Log::Any qw( $log ); 33 | use Path::Tiny; 34 | use Types::Common::String -types; 35 | use Types::Standard -types; 36 | 37 | use Moo; 38 | use strictures 2; 39 | use namespace::clean; 40 | 41 | sub _filter_args { 42 | my ($self, $args) = @_; 43 | 44 | return { 45 | map { $_ => $args->{$_} } 46 | grep { $args->{$_} } 47 | keys %$args 48 | }; 49 | } 50 | 51 | =head1 ARGUMENTS 52 | 53 | =head2 file 54 | 55 | The file to load configuration from. The file should hold valid JSON. 56 | 57 | By default this will be set to C<.gitlab-api-v4-config> in the current 58 | user's home directory. 59 | 60 | This can be overridden with the C environment 61 | variable or the C<--config-file=...> command line argument. 62 | 63 | =cut 64 | 65 | has file => ( 66 | is => 'lazy', 67 | isa => NonEmptySimpleStr, 68 | ); 69 | sub _build_file { 70 | my ($self) = @_; 71 | 72 | my $file = $self->opt_args->{config_file} 73 | || $self->env_args->{config_file}; 74 | return $file if $file; 75 | 76 | my ($home) = ( getpwuid($<) )[7]; 77 | return '' . path( $home )->child('.gitlab-api-v4-config'); 78 | } 79 | 80 | =head1 ATTRIBUTES 81 | 82 | =head2 opt_args 83 | 84 | Returns a hashref of arguments derived from command line options. 85 | 86 | Supported options are: 87 | 88 | --config_file=... 89 | --url=... 90 | --private-token=... 91 | --access-token=... 92 | --retries=... 93 | 94 | Note that the options are read from, and removed from, C<@ARGV>. Due 95 | to this the arguments are saved internally and re-used for all instances 96 | of this class so that there are no weird race conditions. 97 | 98 | =cut 99 | 100 | has opt_args => ( 101 | is => 'rwp', 102 | isa => HashRef, 103 | default => sub{ {} }, 104 | ); 105 | 106 | =head2 env_args 107 | 108 | Returns a hashref of arguments derived from environment variables. 109 | 110 | Supported environment variables are: 111 | 112 | GITLAB_API_V4_CONFIG_FILE 113 | GITLAB_API_V4_URL 114 | GITLAB_API_V4_PRIVATE_TOKEN 115 | GITLAB_API_V4_ACCESS_TOKEN 116 | GITLAB_API_V4_RETRIES 117 | 118 | =cut 119 | 120 | has env_args => ( 121 | is => 'lazy', 122 | isa => HashRef, 123 | ); 124 | sub _build_env_args { 125 | my ($self) = @_; 126 | 127 | return $self->_filter_args({ 128 | config_file => $ENV{GITLAB_API_V4_CONFIG_FILE}, 129 | url => $ENV{GITLAB_API_V4_URL}, 130 | private_token => $ENV{GITLAB_API_V4_PRIVATE_TOKEN}, 131 | access_token => $ENV{GITLAB_API_V4_ACCESS_TOKEN}, 132 | retries => $ENV{GITLAB_API_V4_RETRIES}, 133 | }); 134 | } 135 | 136 | =head2 file_args 137 | 138 | Returns a hashref of arguments gotten by decoding the JSON in the L. 139 | 140 | =cut 141 | 142 | has file_args => ( 143 | is => 'lazy', 144 | isa => HashRef, 145 | ); 146 | sub _build_file_args { 147 | my ($self) = @_; 148 | 149 | my $file = $self->file(); 150 | return {} if !-r $file; 151 | 152 | $file = path( $file ); 153 | $log->debugf( 'Loading configuration for GitLab::API::v4 from: %s', $file->absolute() ); 154 | my $json = $file->slurp(); 155 | my $data = decode_json( $json ); 156 | 157 | return $self->_filter_args( $data ); 158 | } 159 | 160 | =head2 args 161 | 162 | Returns a final, combined, hashref of arguments containing everything in 163 | L, L, and L. If there are any duplicate 164 | arguments then L has highest precedence, L is next, and 165 | at the bottom is L. 166 | 167 | =cut 168 | 169 | sub args { 170 | my ($self) = @_; 171 | 172 | return { 173 | %{ $self->file_args() }, 174 | %{ $self->env_args() }, 175 | %{ $self->opt_args() }, 176 | }; 177 | } 178 | 179 | =head1 METHODS 180 | 181 | =head2 get_options 182 | 183 | =cut 184 | 185 | sub get_options { 186 | my ($self, @extra) = @_; 187 | 188 | Getopt::Long::Configure(qw( 189 | gnu_getopt no_ignore_case 190 | )); 191 | 192 | my $opt_args = {}; 193 | 194 | GetOptions( 195 | 'config-file=s' => \$opt_args->{config_file}, 196 | 'url=s' => \$opt_args->{url}, 197 | 'private-token=s' => \$opt_args->{private_token}, 198 | 'access-token=s' => \$opt_args->{access_token}, 199 | 'retries=i' => \$opt_args->{retries}, 200 | @extra, 201 | ) or die('Unable to process options!'); 202 | 203 | $opt_args = $self->_filter_args( $opt_args ); 204 | 205 | $self->_set_opt_args( $opt_args ); 206 | 207 | return; 208 | } 209 | 210 | =head2 configure 211 | 212 | When called this method interactively prompts the user for argument values 213 | and then encodes them as JSON and stores them in L. The file will 214 | be chmod'ed C<0600> so that only the current user may read or write to the 215 | file. 216 | 217 | =cut 218 | 219 | sub configure { 220 | my ($self) = @_; 221 | 222 | my $url = prompt( 223 | 'Full URL to a v4 GitLab API:', 224 | '-stdio', '-verbatim', 225 | ); 226 | 227 | my $private_token = prompt( 228 | 'Private Token:', 229 | -echo=>'', 230 | '-stdio', '-verbatim', 231 | ); 232 | 233 | my $access_token = prompt( 234 | 'Access Token:', 235 | -echo=>'', 236 | '-stdio', '-verbatim', 237 | ); 238 | 239 | my $json = JSON::MaybeXS->new(pretty => 1, canonical => 1) 240 | ->encode({ 241 | $url ? (url=>$url) : (), 242 | $private_token ? (private_token=>$private_token) : (), 243 | $access_token ? (access_token=>$access_token) : (), 244 | }); 245 | 246 | my $file = path( $self->file() ); 247 | $file->touch(); 248 | $file->chmod( 0600 ); 249 | $file->append( {truncate=>1}, $json ); 250 | 251 | $log->infof( 'Configuration for GitLab::API::v4 saved to: %s', $file->absolute() ); 252 | 253 | return; 254 | } 255 | 256 | 1; 257 | __END__ 258 | 259 | =head1 SUPPORT 260 | 261 | See L. 262 | 263 | =head1 AUTHORS 264 | 265 | See L. 266 | 267 | =head1 LICENSE 268 | 269 | See L. 270 | 271 | =cut 272 | 273 | -------------------------------------------------------------------------------- /t/regression.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strictures 2; 3 | 4 | use Test2::Require::AuthorTesting; 5 | use Test2::V0; 6 | 7 | use Log::Any::Adapter 'TAP'; 8 | use MIME::Base64 qw( decode_base64 ); 9 | use Path::Tiny; 10 | 11 | use GitLab::API::v4::Config; 12 | use GitLab::API::v4::WWWClient; 13 | use GitLab::API::v4; 14 | 15 | my $config = GitLab::API::v4::Config->new(); 16 | my $api = GitLab::API::v4->new( $config->args() ); 17 | 18 | subtest projects => sub{ 19 | my $stamp = time(); 20 | my $project_name = "gitlab-api-v4-test-$stamp"; 21 | 22 | my $created_project = $api->create_project( 23 | { name=>$project_name }, 24 | ); 25 | ok( $created_project, 'project created' ); 26 | 27 | my $project_id = $created_project->{id}; 28 | my $project = $api->project( $project_id ); 29 | ok( $project, 'project found' ); 30 | 31 | subtest upload_file_to_project => sub{ 32 | my $file = Path::Tiny->tempfile( SUFFIX => '.txt' ); 33 | my $file_content = 'Hello GitLab, this is a test of ' . ref($api) . '.'; 34 | $file->spew( $file_content ); 35 | 36 | my $upload = $api->upload_file_to_project( 37 | $project_id, 38 | { file=>"$file" }, 39 | ); 40 | ok( $upload->{url}, 'got an upload response' ); 41 | 42 | my $www_base_url = $api->url(); 43 | $www_base_url =~ s{/api/v4.*$}{}; 44 | 45 | my $www_client = GitLab::API::v4::WWWClient->new( 46 | url => $www_base_url, 47 | ); 48 | 49 | $www_client->sign_in( 50 | 'root', 51 | $ENV{GITLAB_API_V4_ROOT_PASSWORD}, 52 | ); 53 | 54 | my $project_path = $project->{path_with_namespace}; 55 | my $upload_path = $upload->{url}; 56 | my $download_path = "$project_path/$upload_path"; 57 | 58 | my $res = $www_client->get( $download_path ); 59 | is( $res->{content}, $file_content, 'upload_file_to_project worked' ); 60 | }; 61 | 62 | subtest 'file methods' => sub{ 63 | $api->create_file( 64 | $project_id, 65 | 'foo/bar.txt', 66 | { 67 | branch => 'master', 68 | content => 'Test of create file.', 69 | commit_message => 'This is a commit.', 70 | }, 71 | ); 72 | 73 | my $file = $api->file( 74 | $project_id, 75 | 'foo/bar.txt', 76 | { ref=>'master' }, 77 | ); 78 | is( 79 | decode_base64( $file->{content} ), 80 | 'Test of create file.', 81 | 'created file is there; and looks right', 82 | ); 83 | 84 | my $content = $api->raw_file( 85 | $project_id, 86 | 'foo/bar.txt', 87 | { ref=>'master' }, 88 | ); 89 | is( 90 | $content, 91 | 'Test of create file.', 92 | 'able to retrieve the file raw', 93 | ); 94 | 95 | $api->edit_file( 96 | $project_id, 97 | 'foo/bar.txt', 98 | { 99 | branch => 'master', 100 | content => 'Test of edit file.', 101 | commit_message => 'This is the next commit.', 102 | }, 103 | ); 104 | my $edited_file = $api->file( 105 | $project_id, 106 | 'foo/bar.txt', 107 | { ref=>'master' }, 108 | ); 109 | is( 110 | decode_base64( $edited_file->{content} ), 111 | 'Test of edit file.', 112 | 'editing a file worked', 113 | ); 114 | 115 | $api->delete_file( 116 | $project_id, 117 | 'foo/bar.txt', 118 | { 119 | branch => 'master', 120 | commit_message => 'This is the last commit.', 121 | }, 122 | ); 123 | $file = $api->file( 124 | $project_id, 125 | 'foo/bar.txt', 126 | { ref=>'master' }, 127 | ); 128 | is( 129 | $file, undef, 130 | 'file was deleted', 131 | ); 132 | }; 133 | 134 | subtest hooks => sub{ 135 | my $hook = $api->create_project_hook( 136 | $project->{id}, 137 | { url=>'http://example.com/gitlab-hook-1' }, 138 | ); 139 | ok( $hook, 'create_project_hook returned the hook' ); 140 | 141 | $hook = $api->edit_project_hook( 142 | $project->{id}, $hook->{id}, 143 | { url=>'http://example.com/gitlab-hook-2' }, 144 | ); 145 | ok( $hook, 'edit_project_hook returned the hook' ); 146 | my $hook_id = $hook->{id}; 147 | 148 | $hook = $api->project_hook( $project->{id}, $hook_id ); 149 | ok( $hook, 'project_hook returned the hook' ); 150 | is( $hook->{url}, 'http://example.com/gitlab-hook-2', 'hook looks right' ); 151 | 152 | $api->delete_project_hook( $project->{id}, $hook_id ); 153 | $hook = $api->project_hook( $project->{id}, $hook_id ); 154 | ok( (!$hook), 'delete_project_hook seems to have worked' ); 155 | 156 | like( 157 | dies { $api->delete_project_hook( $project->{id}, $hook_id ) }, 158 | qr{\b404\b}, 159 | 'a subsequent delete_project_hook throws', 160 | ); 161 | }; 162 | 163 | $api->delete_project( $project_id ); 164 | pass 'project deleted'; 165 | }; 166 | 167 | subtest users => sub{ 168 | my $stamp = time(); 169 | my $username = "gitlab-api-v4-test-$stamp"; 170 | $api->create_user({ 171 | username => $username, 172 | email => "$username\@example.com", 173 | password => 'd5fzHF7tfgh', 174 | name => 'GitLabAPIv4 Test', 175 | }); 176 | pass 'user created'; 177 | 178 | my $users = $api->users({ username => $username }); 179 | is( @$users+0, 1, 'one user found' ); 180 | 181 | my $user = shift @$users; 182 | is( $user->{username}, $username, 'user has correct username' ); 183 | die 'Incorrect user found' if $user->{username} ne $username; 184 | 185 | my $user_id = $user->{id}; 186 | ok( $api->block_user($user_id), 'user blocked' ); 187 | ok( (!$api->block_user($user_id)), 'user cannot be blocked again' ); 188 | ok( $api->unblock_user($user_id), 'user unblocked' ); 189 | ok( (!$api->unblock_user($user_id)), 'user cannot be unblocked again' ); 190 | 191 | $api->delete_user($user_id); 192 | pass 'user deleted'; 193 | }; 194 | 195 | subtest failures => sub{ 196 | is( $api->user( 12345678 ), undef, 'GETing an unknown entity returns undef' ); 197 | my $err_re = qr{^Error PUTing \S+/users/12345678 \(HTTP 404\): Not Found \{"message":"404 User Not Found"\}}; 198 | like( dies{ $api->edit_user( 12345678, {} ) }, $err_re, 'POSTing an unknown entity throws a specific exception' ); 199 | }; 200 | 201 | done_testing; 202 | 203 | sub join_paths { 204 | my @paths = @_; 205 | 206 | return() if !@paths; 207 | return @paths if @paths==1; 208 | 209 | my $first = shift @paths; 210 | $first =~ s{/$}{}; 211 | 212 | my $last = pop @paths; 213 | $last =~ s{^/}{}; 214 | 215 | @paths = ( 216 | map { $_ =~ s{^/?(.*?)/?$}{$1}; $_ } 217 | @paths 218 | ); 219 | 220 | return join('/', $first, @paths, $last); 221 | } 222 | -------------------------------------------------------------------------------- /author/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sections: 3 | - award_emoji: 4 | head: Award Emoji 5 | doc_url: https://docs.gitlab.com/ce/api/award_emoji.html 6 | - branches: 7 | head: Branches 8 | doc_url: https://docs.gitlab.com/ce/api/branches.html 9 | - broadcast_messages: 10 | head: Broadcast Messages 11 | doc_url: https://docs.gitlab.com/ce/api/broadcast_messages.html 12 | - project_level_variables: 13 | head: Project-level Variables 14 | doc_url: https://docs.gitlab.com/ce/api/project_level_variables.html 15 | - group_level_variables: 16 | head: Group-level Variables 17 | doc_url: https://docs.gitlab.com/ce/api/group_level_variables.html 18 | - snippets: 19 | head: Snippets 20 | doc_url: https://docs.gitlab.com/ce/api/snippets.html 21 | - commits: 22 | head: Commits 23 | doc_url: https://docs.gitlab.com/ce/api/commits.html 24 | - container_registry: 25 | head: Container Registry 26 | doc_url: https://docs.gitlab.com/ce/api/container_registry.html 27 | - custom_attributes: 28 | head: Custom Attributes 29 | doc_url: https://docs.gitlab.com/ce/api/custom_attributes.html 30 | - deployments: 31 | head: Deployments 32 | doc_url: https://docs.gitlab.com/ce/api/deployments.html 33 | - deploy_keys: 34 | head: Deploy Keys 35 | doc_url: https://docs.gitlab.com/ce/api/deploy_keys.html 36 | - environments: 37 | head: Environments 38 | doc_url: https://docs.gitlab.com/ce/api/environments.html 39 | - events: 40 | head: Events 41 | doc_url: https://docs.gitlab.com/ce/api/events.html 42 | - features: 43 | head: Feature flags 44 | doc_url: https://docs.gitlab.com/ce/api/features.html 45 | - gitignores_templates: 46 | head: Gitignores 47 | doc_url: https://docs.gitlab.com/ce/api/templates/gitignores.html 48 | - gitlab_ci_ymls_templates: 49 | head: GitLab CI YMLs 50 | doc_url: https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html 51 | - groups: 52 | head: Groups 53 | doc_url: https://docs.gitlab.com/ce/api/groups.html 54 | - group_access_requests: 55 | head: Group access requests 56 | doc_url: https://docs.gitlab.com/ce/api/access_requests.html 57 | - group_badges: 58 | head: Group badges 59 | doc_url: https://docs.gitlab.com/ce/api/group_badges.html 60 | - group_members: 61 | head: Group members 62 | doc_url: https://docs.gitlab.com/ce/api/members.html 63 | - issues: 64 | head: Issues 65 | doc_url: https://docs.gitlab.com/ce/api/issues.html 66 | - boards: 67 | head: Issue Boards 68 | doc_url: https://docs.gitlab.com/ce/api/boards.html 69 | - group_boards: 70 | head: Group Issue Boards 71 | doc_url: https://docs.gitlab.com/ce/api/group_boards.html 72 | - jobs: 73 | head: Jobs 74 | doc_url: https://docs.gitlab.com/ce/api/jobs.html 75 | - keys: 76 | head: Keys 77 | doc_url: https://docs.gitlab.com/ce/api/keys.html 78 | - labels: 79 | head: Labels 80 | doc_url: https://docs.gitlab.com/ce/api/labels.html 81 | - markdown: 82 | head: Markdown 83 | doc_url: https://docs.gitlab.com/ce/api/markdown.html 84 | - merge_requests: 85 | head: Merge requests 86 | doc_url: https://docs.gitlab.com/ce/api/merge_requests.html 87 | - milestones: 88 | head: Milestones 89 | doc_url: https://docs.gitlab.com/ce/api/milestones.html 90 | - group_milestones: 91 | head: Group milestones 92 | doc_url: https://docs.gitlab.com/ce/api/group_milestones.html 93 | - namespaces: 94 | head: Namespaces 95 | doc_url: https://docs.gitlab.com/ce/api/namespaces.html 96 | - notes: 97 | head: Notes 98 | doc_url: https://docs.gitlab.com/ce/api/notes.html 99 | - discussions: 100 | head: Discussions 101 | doc_url: https://docs.gitlab.com/ce/api/discussions.html 102 | - resource_label_events: 103 | head: Resource label events 104 | doc_url: https://docs.gitlab.com/ce/api/resource_label_events.html 105 | - notification_settings: 106 | head: Notification settings 107 | doc_url: https://docs.gitlab.com/ce/api/notification_settings.html 108 | - licenses_templates: 109 | head: Licenses 110 | doc_url: https://docs.gitlab.com/ce/api/templates/licenses.html 111 | - page_domains: 112 | head: Pages domains 113 | doc_url: https://docs.gitlab.com/ce/api/pages_domains.html 114 | - pipelines: 115 | head: Pipelines 116 | doc_url: https://docs.gitlab.com/ce/api/pipelines.html 117 | - pipeline_triggers: 118 | head: Pipeline triggers 119 | doc_url: https://docs.gitlab.com/ce/api/pipeline_triggers.html 120 | - pipeline_schedules: 121 | head: Pipeline schedules 122 | doc_url: https://docs.gitlab.com/ce/api/pipeline_schedules.html 123 | - projects: 124 | head: Projects 125 | doc_url: https://docs.gitlab.com/ce/api/projects.html 126 | - project_access_requests: 127 | head: Project access requests 128 | doc_url: https://docs.gitlab.com/ce/api/access_requests.html 129 | - project_badges: 130 | head: Project badges 131 | doc_url: https://docs.gitlab.com/ce/api/project_badges.html 132 | - project_import_export: 133 | head: Project import/export 134 | doc_url: https://docs.gitlab.com/ce/api/project_import_export.html 135 | - project_members: 136 | head: Project members 137 | doc_url: https://docs.gitlab.com/ce/api/members.html 138 | - project_snippets: 139 | head: Project snippets 140 | doc_url: https://docs.gitlab.com/ce/api/project_snippets.html 141 | - protected_branches: 142 | head: Protected branches 143 | doc_url: https://docs.gitlab.com/ce/api/protected_branches.html 144 | - protected_tags: 145 | head: Protected tags 146 | doc_url: https://docs.gitlab.com/ce/api/protected_tags.html 147 | - releases: 148 | head: Releases 149 | doc_url: https://docs.gitlab.com/ce/api/releases/index.html 150 | - release_links: 151 | head: Release Links 152 | doc_url: https://docs.gitlab.com/ce/api/releases/links.html 153 | - remote_mirrors: 154 | head: Remote Mirrors 155 | doc_url: https://docs.gitlab.com/ce/api/remote_mirrors.html 156 | - repositories: 157 | head: Repositories 158 | doc_url: https://docs.gitlab.com/ce/api/repositories.html 159 | - repository_files: 160 | head: Repository files 161 | doc_url: https://docs.gitlab.com/ce/api/repository_files.html 162 | - runners: 163 | head: Runners 164 | doc_url: https://docs.gitlab.com/ce/api/runners.html 165 | - search: 166 | head: Search 167 | doc_url: https://docs.gitlab.com/ce/api/search.html 168 | - services: 169 | head: Services 170 | doc_url: https://docs.gitlab.com/ce/api/services.html 171 | - settings: 172 | head: Application settings 173 | doc_url: https://docs.gitlab.com/ce/api/settings.html 174 | - statistics: 175 | head: Application statistics 176 | doc_url: https://docs.gitlab.com/ce/api/statistics.html 177 | - sidekiq_metrics: 178 | head: Sidekiq Metrics 179 | doc_url: https://docs.gitlab.com/ce/api/sidekiq_metrics.html 180 | - system_hooks: 181 | head: System hooks 182 | doc_url: https://docs.gitlab.com/ce/api/system_hooks.html 183 | - tags: 184 | head: Tags 185 | doc_url: https://docs.gitlab.com/ce/api/tags.html 186 | - todos: 187 | head: Todos 188 | doc_url: https://docs.gitlab.com/ce/api/todos.html 189 | - users: 190 | head: Users 191 | doc_url: https://docs.gitlab.com/ce/api/users.html 192 | - lint: 193 | head: Validate the .gitlab-ci.yml 194 | doc_url: https://docs.gitlab.com/ce/api/lint.html 195 | - version: 196 | head: Version 197 | doc_url: https://docs.gitlab.com/ce/api/version.html 198 | - wikis: 199 | head: Wikis 200 | doc_url: https://docs.gitlab.com/ce/api/wikis.html 201 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for Perl extension GitLab-API-v4. 2 | 3 | {{$NEXT}} 4 | 5 | - Retry on 429 (rate limited) responses. 6 | - Add a retry_wait with a default of 10 (seconds). 7 | 8 | 0.27 2023-06-07T20:50:56Z 9 | 10 | - Add run_pipeline_schedule. 11 | - Add share_group_with_group and unshare_group_with_group. 12 | - Add approve_merge_request and unapprove_merge_request 13 | - Add approve_user and reject_user. 14 | - Add ban_user and unban_user. 15 | - Add activate_user and deactivate_user. 16 | - Allow multi-line arguments. 17 | - Use JSON::MaybeXS instead of JSON. 18 | - Add verify_SSL=>1 to HTTP::Tiny to verify https server identity. 19 | - Handle multiple cookies correctly when there is more than one. 20 | - Live tests now run against GitLab CE 16.0.3 (was 11.10.4). 21 | - Add the the remote_mirrors, create_remote_mirror, and 22 | edit_remote_mirror methods. 23 | - Fixed incorrect license messaging in POD. 24 | 25 | 0.26 2021-01-30T07:10:57Z 26 | 27 | - Changed licensing terms to be the same as Perl 5. 28 | - Add the user_memeberships method. 29 | - Remove file renaming before upload. 30 | - Provide a .editorconfig instead of .lvimrc. 31 | 32 | 0.25 2020-02-12T22:20:48Z 33 | 34 | - Add the delete_pipeline method. 35 | 36 | 0.24 2020-02-12T22:09:39Z 37 | 38 | - Add all the container registry methods. 39 | - Add http_tiny_request and http_tiny_response to ::RESTClient. This 40 | should help people debug issues easier. 41 | 42 | 0.23 2019-10-18T19:55:06Z 43 | 44 | - Fix delete_runner so that it doesn't try to decode the response. 45 | - Any 204 response now just blanket returns undef. 46 | - More contextual JSON decoding error messages. 47 | 48 | 0.22 2019-09-13T15:02:28Z 49 | 50 | - Add the statistics method. 51 | - The delete_user method can now take parameters, allowing for use of 52 | the hard_delete parameter. 53 | 54 | 0.21 2019-08-24T18:56:13Z 55 | 56 | - The release endpoint returns a 403 on an unknown tag rather than a 57 | 404 like it should. This condition is now detected and treated 58 | like a 404. 59 | 60 | 0.20 2019-07-23T21:42:37Z 61 | 62 | - The preexisting create_release and edit_release methods have been 63 | renamed to create_tag_release and update_tag_release to make room 64 | for the new releases API. 65 | - Added methods for the new (added in GitLab 11.7) releases and 66 | release links APIs. 67 | 68 | 0.19 2019-05-17T20:40:22Z 69 | 70 | - Live tests now run against GitLab CE 11.10.4 (was 11.2.3). 71 | - Add GitLab::API::v4::WWWClient. 72 | - Add GitLab::API::v4::Mock for writing unit tests. 73 | - Switch to the GNU General Public License version 3. 74 | 75 | 0.18 2019-04-01T04:35:02Z 76 | 77 | - Add SUPPORT section to the docs. 78 | - The archive method no longer tries to decode the response. 79 | - Add the commit_refs method. 80 | - Add the trigger_pipeline method. 81 | - Document some create_pipeline method parameters. 82 | 83 | 0.17 2019-02-20T22:47:58Z 84 | 85 | - Require perl 5.10.1 as I give up trying to support 5.8 due to other 86 | modules requiring 5.10 and strictures/multidimensional issues. 87 | - Simplify VERSION declarations. 88 | 89 | 0.16 2019-02-18T06:21:15Z 90 | 91 | - Migrate build tooling from Dist::Zilla to Minilla. 92 | 93 | 0.15 2019-01-09 94 | 95 | - BREAKING CHANGE: gitlab-api-v4 now takes parameters in the form of 96 | param:value rather than --param=value. This is to avoid foot-gun 97 | ambiguities when options have the same names as parameters (such 98 | as --url). 99 | - Add --pretty and --canonical options to gitlab-api-v4. 100 | 101 | 0.14 2018-12-04 102 | 103 | - Add all_project_members and all_group_members methods. 104 | - gitlab-api-v4 now handles -- like you'd expect your typical linux 105 | tooling would. 106 | 107 | 0.13 2018-11-08 108 | 109 | - group() now accepts parameters. 110 | - Add the transfer_project_to_namespace() method. 111 | 112 | 0.12 2018-09-11 113 | 114 | - Added API methods for Protected Tags, Search, Project 115 | Import/Export, Project Badges, Resource Label Events, Discussions, 116 | Markdown, Group Issue Boards, Group Badges, and Snippets. 117 | - Reorganized project/group members/access requests sections to 118 | better match the official API. 119 | - Renamed the various project snippet methods to have "project" in 120 | their name. 121 | - Added constants for the snippet visibility levels. 122 | - Overhauled the POD heading layout in order to avoid the massive 123 | TOC. 124 | - Fix minor typo. 125 | 126 | 0.11 2018-09-01 127 | 128 | - Fixed the raw_file method to not try to decode the respository file 129 | contents and instead just return the undecoded response content. 130 | - Renamed the POD section "FILE METHODS" to 131 | "REPOSITORY FILE METHODS", to better match the GitLab API docs. 132 | - Add the hidden NO_ACCESS constant to ::Constants. 133 | - Live tests now run against GitLab CE 11.2.3 (was 10.2.2). 134 | 135 | 0.10 2018-06-05 136 | 137 | - Fix project_hook() so it uses the correct path. 138 | - create_project_hook() and edit_project_hook() now return their 139 | decoded response (the hook). 140 | - delete_project_hook() no longer tries to return anything as a 141 | successful delete is a 204 (no content) and a failed one is a 404. 142 | - Tests for all of the above. 143 | - Fix typo. 144 | 145 | 0.09 2018-05-28 146 | 147 | - Ensure chmod 600 of ~/.gitlab-api-v4-config is maintained. 148 | - Enable and fix upload_file_to_project by using 149 | HTTP::Tiny::Multipart. 150 | - Individual methods may now have a custom note added to their POD. 151 | - Added a self-contained unit test, unit.t, which can be expanded on 152 | in the future. This is unlike regression.t which requires that a 153 | GitLab installation be available. 154 | - Added the rest_client_class argument. 155 | 156 | 0.08 2018-05-14 157 | 158 | - Many API endpoints which return lists and did not support 159 | parameters now do in order for pagination arguments, and the 160 | paginator, to be used with them. 161 | - Add project_languages method. 162 | - Remove upload_file_to_project since it doesn't work. Will re-add 163 | when it is made to work later. 164 | 165 | 0.07 2018-05-10 166 | 167 | - Completely overhaul ::RestClient to use HTTP::Tiny instead of 168 | Role::REST::Client. This change will make it much easier to alter 169 | the behavior of the HTTP communication. 170 | - ::RestClient now supports the ability to decode non-ref JSON, such 171 | as unblock_user which returns just a JSON boolean. 172 | - block_user and unblock_user now return their decoded response. 173 | - delete_user is no longer expected to return anything. 174 | - Response decoding will now only be attempted for methods which are 175 | expected to return something. Slight optimization. 176 | - A 404 on a non-returning method throws an exception now, matching 177 | the docs. 178 | - The *_token arguments are now stored in a closure to help users 179 | avoid accidentally dumping them somewhere such as logs. 180 | - Added all methods for the TODO API. 181 | 182 | 0.06 2018-04-09 183 | 184 | - Add the project_service method. 185 | 186 | 0.05 2018-03-06 187 | 188 | - Fixup gitlab-api-v4 to support parameters who's values are empty or 189 | have whitespace in them. 190 | 191 | 0.04 2018-02-03 192 | 193 | - Support parameters in the project method. 194 | - Allow wide characters when printing the response output in the 195 | gitlab-api-v4 script. 196 | 197 | 0.03 2018-01-12 198 | 199 | - Greatly extended authentication and configuration options for the 200 | gitlab-api-v4 script; added GitLab::API::v4::Config. 201 | - The gitlab-api-v4 script now always outputs JSON (the default was 202 | YAML, with the option to pick other formats). 203 | - Fixed the fork_project method to hit the correct URL. 204 | 205 | 0.02 2017-12-15 206 | 207 | - Fix double-slashes in URL paths. 208 | - Fixed gitlab-api-v4 script to actually work. 209 | 210 | 0.01 2017-12-11 211 | 212 | - Added the sudo method and the sudo_user argument. 213 | - Split the token arguments into the access_token and private_token 214 | arguments. 215 | - Removed the login/email/password arguments as they are not 216 | supported by v4. 217 | - Many many methods added/removed/renamed/modified. 218 | - Imported GitLab::API::v3 @ 219 | fb9253c58b68ca1be71feedf12c1d6004c8ba8d6. 220 | 221 | -------------------------------------------------------------------------------- /lib/GitLab/API/v4/RESTClient.pm: -------------------------------------------------------------------------------- 1 | package GitLab::API::v4::RESTClient; 2 | our $VERSION = '0.27'; 3 | 4 | =encoding utf8 5 | 6 | =head1 NAME 7 | 8 | GitLab::API::v4::RESTClient - The HTTP client that does the heavy lifting. 9 | 10 | =head1 DESCRIPTION 11 | 12 | Currently this class uses L and L to do its job. 13 | This may change, and the interface may change, so documentation is lacking in 14 | order to not mislead people. 15 | 16 | If you do want to customize how this class works then take a look at the 17 | source. 18 | 19 | =head1 ATTRIBUTES 20 | 21 | =head1 http_tiny_request 22 | 23 | my $req = $api->rest_client->http_tiny_request(); 24 | 25 | The most recent request arrayref as passed to L. 26 | 27 | If this is C then no request has been made. 28 | 29 | =head1 http_tiny_response 30 | 31 | my $res = $api->rest_client->http_tiny_response(); 32 | 33 | The most recent response hashref as passed back from L. 34 | 35 | If this is C and L is defined then no response was received 36 | and you will have encountered an error when making the request 37 | 38 | =cut 39 | 40 | use Carp qw(); 41 | use HTTP::Tiny::Multipart; 42 | use HTTP::Tiny; 43 | use JSON::MaybeXS; 44 | use Log::Any qw( $log ); 45 | use Path::Tiny; 46 | use Try::Tiny; 47 | use Types::Common::Numeric -types; 48 | use Types::Common::String -types; 49 | use Types::Standard -types; 50 | use URI::Escape; 51 | use URI; 52 | 53 | use Moo; 54 | use strictures 2; 55 | use namespace::clean; 56 | 57 | sub croak { 58 | local $Carp::Internal{ 'GitLab::API::v4' } = 1; 59 | local $Carp::Internal{ 'GitLab::API::v4::RESTClient' } = 1; 60 | 61 | return Carp::croak( @_ ); 62 | } 63 | 64 | sub croakf { 65 | my $msg = shift; 66 | $msg = sprintf( $msg, @_ ); 67 | return croak( $msg ); 68 | } 69 | 70 | has _clean_base_url => ( 71 | is => 'lazy', 72 | init_arg => undef, 73 | builder => '_build_clean_base_url', 74 | ); 75 | sub _build_clean_base_url { 76 | my ($self) = @_; 77 | my $url = $self->base_url(); 78 | 79 | # Remove any leading slash so that request() does not build URLs 80 | # with double slashes when joining the base_url with the path. 81 | # If double slashes were allowed then extra unecessary redirects 82 | # could happen. 83 | $url =~ s{/+$}{}; 84 | 85 | return URI->new( $url )->canonical(); 86 | } 87 | 88 | has base_url => ( 89 | is => 'ro', 90 | isa => NonEmptySimpleStr, 91 | required => 1, 92 | ); 93 | 94 | has retries => ( 95 | is => 'ro', 96 | isa => PositiveOrZeroInt, 97 | default => 0, 98 | ); 99 | 100 | has retry_wait => ( 101 | is => 'ro', 102 | isa => PositiveOrZeroInt, 103 | default => 10, 104 | ); 105 | 106 | has http_tiny => ( 107 | is => 'lazy', 108 | isa => InstanceOf[ 'HTTP::Tiny' ], 109 | ); 110 | sub _build_http_tiny { 111 | return HTTP::Tiny->new( verify_SSL => 1 ); 112 | } 113 | 114 | has json => ( 115 | is => 'lazy', 116 | isa => HasMethods[ 'encode', 'decode' ], 117 | ); 118 | sub _build_json { 119 | return JSON::MaybeXS->new(utf8 => 1, allow_nonref => 1); 120 | } 121 | 122 | has http_tiny_request => ( 123 | is => 'ro', 124 | writer => '_set_request', 125 | clearer => '_clear_request', 126 | init_arg => undef, 127 | ); 128 | 129 | has http_tiny_response => ( 130 | is => 'ro', 131 | writer => '_set_response', 132 | clearer => '_clear_response', 133 | init_arg => undef, 134 | ); 135 | 136 | # The purpose of this method is for tests to have a place to inject themselves. 137 | sub _http_tiny_request { 138 | my ($self, $req_method, $req) = @_; 139 | 140 | return $self->http_tiny->$req_method( @$req ); 141 | } 142 | 143 | sub request { 144 | my ($self, $verb, $raw_path, $path_vars, $options) = @_; 145 | 146 | $self->_clear_request(); 147 | $self->_clear_response(); 148 | 149 | $options = { %{ $options || {} } }; 150 | my $query = delete $options->{query}; 151 | my $content = delete $options->{content}; 152 | my $headers = $options->{headers} = { %{ $options->{headers} || {} } }; 153 | 154 | # Convert foo/:bar/baz into foo/%s/baz. 155 | my $path = $raw_path; 156 | $path =~ s{:[^/]+}{%s}g; 157 | # sprintf will throw if the number of %s doesn't match the size of @$path_vars. 158 | # Might be nice to catch that and provide a better error message, but that should 159 | # never happen as the API methods verify the argument size before we get here. 160 | $path = sprintf($path, (map { uri_escape($_) } @$path_vars)) if @$path_vars; 161 | 162 | $log->tracef( 'Making %s request against %s', $verb, $path ); 163 | 164 | my $url = $self->_clean_base_url->clone(); 165 | $url->path( $url->path() . '/' . $path ); 166 | $url->query_form( $query ) if defined $query; 167 | $url = "$url"; # No more changes to the url from this point forward. 168 | 169 | my $req_method = 'request'; 170 | my $req = [ $verb, $url, $options ]; 171 | 172 | if ($verb eq 'POST' and ref($content) eq 'HASH' and $content->{file}) { 173 | $content = { %$content }; 174 | my $file = path( delete $content->{file} ); 175 | 176 | unless (-f $file and -r $file) { 177 | local $Carp::Internal{ 'GitLab::API::v4' } = 1; 178 | local $Carp::Internal{ 'GitLab::API::v4::RESTClient' } = 1; 179 | croak "File $file is not readable"; 180 | } 181 | 182 | # Might as well mask the filename, but leave the extension. 183 | my $filename = $file->basename(); # foo/bar.txt => bar.txt 184 | 185 | my $data = { 186 | file => { 187 | filename => $filename, 188 | content => $file->slurp(), 189 | }, 190 | }; 191 | 192 | $req->[0] = $req->[1]; # Replace method with url. 193 | $req->[1] = $data; # Put data where url was. 194 | # So, req went from [$verb,$url,$options] to [$url,$data,$options], 195 | # per the post_multipart interface. 196 | 197 | $req_method = 'post_multipart'; 198 | $content = undef if ! %$content; 199 | } 200 | 201 | if (ref $content) { 202 | $content = $self->json->encode( $content ); 203 | $headers->{'content-type'} = 'application/json'; 204 | $headers->{'content-length'} = length( $content ); 205 | } 206 | 207 | $options->{content} = $content if defined $content; 208 | 209 | $self->_set_request( $req ); 210 | 211 | my $res; 212 | my $tries_left = $self->retries + 1; 213 | while ($tries_left) { 214 | $res = $self->_http_tiny_request( $req_method, $req ); 215 | $tries_left--; 216 | last unless $tries_left; 217 | 218 | # Retry if we get a 5xx (error) or 429 (rate limited) 219 | my $status = $res->{status}; 220 | last unless $status =~ m{^5} or $status == 429; 221 | my $wait = $self->retry_wait; 222 | $log->warn("Request failed with a $status status; retrying in $wait seconds..."); 223 | sleep $wait if $wait; 224 | } 225 | 226 | $self->_set_response( $res ); 227 | 228 | if ($res->{status} eq '404' and $verb eq 'GET') { 229 | return undef; 230 | } 231 | 232 | # Special case for: 233 | # https://github.com/bluefeet/GitLab-API-v4/issues/35#issuecomment-515533017 234 | if ($res->{status} eq '403' and $verb eq 'GET' and $raw_path eq 'projects/:project_id/releases/:tag_name') { 235 | return undef; 236 | } 237 | 238 | if ($res->{success}) { 239 | return undef if $res->{status} eq '204'; 240 | 241 | my $decode = $options->{decode}; 242 | $decode = 1 if !defined $decode; 243 | return $res->{content} if !$decode; 244 | 245 | return try{ 246 | $self->json->decode( $res->{content} ); 247 | } 248 | catch { 249 | croakf( 250 | 'Error decoding JSON (%s %s %s): ', 251 | $verb, $url, $res->{status}, $_, 252 | ); 253 | }; 254 | } 255 | 256 | my $glimpse = $res->{content} || ''; 257 | $glimpse =~ s{\s+}{ }g; 258 | if ( length($glimpse) > 50 ) { 259 | $glimpse = substr( $glimpse, 0, 50 ); 260 | $glimpse .= '...'; 261 | } 262 | 263 | croakf( 264 | 'Error %sing %s (HTTP %s): %s %s', 265 | $verb, $url, 266 | $res->{status}, ($res->{reason} || 'Unknown'), 267 | $glimpse, 268 | ); 269 | } 270 | 271 | 1; 272 | __END__ 273 | 274 | =head1 SUPPORT 275 | 276 | See L. 277 | 278 | =head1 AUTHORS 279 | 280 | See L. 281 | 282 | =head1 LICENSE 283 | 284 | See L. 285 | 286 | =cut 287 | 288 | -------------------------------------------------------------------------------- /author/header.pm: -------------------------------------------------------------------------------- 1 | package GitLab::API::v4; 2 | our $VERSION = '0.26'; 3 | 4 | =encoding utf8 5 | 6 | =head1 NAME 7 | 8 | GitLab::API::v4 - A complete GitLab API v4 client. 9 | 10 | =head1 SYNOPSIS 11 | 12 | use GitLab::API::v4; 13 | 14 | my $api = GitLab::API::v4->new( 15 | url => $v4_api_url, 16 | private_token => $token, 17 | ); 18 | 19 | my $branches = $api->branches( $project_id ); 20 | 21 | =head1 DESCRIPTION 22 | 23 | This module provides a one-to-one interface with the GitLab 24 | API v4. Much is not documented here as it would just be duplicating 25 | GitLab's own L. 26 | 27 | Note that this distribution also includes the L command-line 28 | interface (CLI). 29 | 30 | =head2 Upgrading 31 | 32 | If you are upgrading from L make sure you read: 33 | 34 | L 35 | 36 | Also, review the C file included in the distribution as it outlines 37 | the changes made to convert the v3 module to v4: 38 | 39 | L 40 | 41 | Finally, be aware that many methods were added, removed, renamed, and/or altered. 42 | If you want to review exactly what was changed you can use GitHub's compare tool: 43 | 44 | L 45 | 46 | Or clone the repo and run this command: 47 | 48 | C 49 | 50 | =head2 Credentials 51 | 52 | Authentication credentials may be defined by setting either the L 53 | or L arguments. 54 | 55 | If no credentials are supplied then the client will be anonymous and greatly 56 | limited in what it can do with the API. 57 | 58 | Extra care has been taken to hide the token arguments behind closures. This way, 59 | if you dump your api object, your tokens won't accidentally leak into places you 60 | don't want them to. 61 | 62 | =head2 Constants 63 | 64 | The GitLab API, in rare cases, uses a hard-coded value to represent a state. 65 | To make life easier the L module exposes 66 | these states as named variables. 67 | 68 | =head2 Exceptions 69 | 70 | The API methods will all throw a useful exception if 71 | an unsuccessful response is received from the API. That is except for 72 | C requests that return a C<404> response - these will return C 73 | for methods that return a value. 74 | 75 | If you'd like to catch and handle these exceptions consider using 76 | L. 77 | 78 | =head2 Logging 79 | 80 | This module uses L and produces some debug messages here 81 | and there, but the most useful bits are the info messages produced 82 | just before each API call. 83 | 84 | =head2 Project ID 85 | 86 | Note that many API calls require a C<$project_id>. This can be 87 | specified as a numeric project C or, in many cases, maybe all cases, 88 | as a C string. The GitLab documentation on 89 | this point is vague. 90 | 91 | =cut 92 | 93 | use Carp qw( croak ); 94 | use GitLab::API::v4::Paginator; 95 | use GitLab::API::v4::RESTClient; 96 | use Log::Any qw( $log ); 97 | use Types::Common::Numeric -types; 98 | use Types::Common::String -types; 99 | use Types::Standard -types; 100 | 101 | use Moo; 102 | use strictures 2; 103 | use namespace::clean; 104 | 105 | sub BUILD { 106 | my ($self) = @_; 107 | 108 | # Ensure any token arguments get moved into their closure before we return 109 | # the built object. 110 | $self->access_token(); 111 | $self->private_token(); 112 | 113 | $log->debugf( "An instance of %s has been created.", ref($self) ); 114 | 115 | return; 116 | } 117 | 118 | sub _call_rest_client { 119 | my ($self, $verb, $path, $path_vars, $options) = @_; 120 | 121 | $options->{headers} = $self->_auth_headers(); 122 | 123 | return $self->rest_client->request( 124 | $verb, $path, $path_vars, $options, 125 | ); 126 | } 127 | 128 | sub _auth_headers { 129 | my ($self) = @_; 130 | my $headers = {}; 131 | 132 | $headers->{'authorization'} = 'Bearer ' . $self->access_token() 133 | if defined $self->access_token(); 134 | $headers->{'private-token'} = $self->private_token() 135 | if defined $self->private_token(); 136 | $headers->{'sudo'} = $self->sudo_user() 137 | if defined $self->sudo_user(); 138 | 139 | return $headers; 140 | } 141 | 142 | sub _clone_args { 143 | my ($self) = @_; 144 | 145 | return { 146 | url => $self->url(), 147 | retries => $self->retries(), 148 | rest_client => $self->rest_client(), 149 | (defined $self->access_token()) ? (access_token=>$self->access_token()) : (), 150 | (defined $self->private_token()) ? (private_token=>$self->private_token()) : (), 151 | }; 152 | } 153 | 154 | sub _clone { 155 | my $self = shift; 156 | 157 | my $class = ref $self; 158 | my $args = { 159 | %{ $self->_clone_args() }, 160 | %{ $class->BUILDARGS( @_ ) }, 161 | }; 162 | 163 | return $class->new( $args ); 164 | } 165 | 166 | # Little utility method that avoids any ambiguity in whether a closer is 167 | # causing circular references. Don't ever pass it a ref. 168 | sub _make_safe_closure { 169 | my ($ret) = @_; 170 | return sub{ $ret }; 171 | } 172 | 173 | =head1 REQUIRED ARGUMENTS 174 | 175 | =head2 url 176 | 177 | The URL to your v4 API endpoint. Typically this will be something 178 | like C. 179 | 180 | =cut 181 | 182 | has url => ( 183 | is => 'ro', 184 | isa => NonEmptySimpleStr, 185 | required => 1, 186 | ); 187 | 188 | =head1 OPTIONAL ARGUMENTS 189 | 190 | =head2 access_token 191 | 192 | A GitLab API OAuth2 token. If set then L may not be set. 193 | 194 | See L. 195 | 196 | =cut 197 | 198 | has _access_token_arg => ( 199 | is => 'ro', 200 | isa => NonEmptySimpleStr, 201 | init_arg => 'access_token', 202 | clearer => '_clear_access_token_arg', 203 | ); 204 | 205 | has _access_token_closure => ( 206 | is => 'lazy', 207 | isa => CodeRef, 208 | init_arg => undef, 209 | builder => '_build_access_token_closure', 210 | ); 211 | sub _build_access_token_closure { 212 | my ($self) = @_; 213 | my $token = $self->_access_token_arg(); 214 | $self->_clear_access_token_arg(); 215 | return _make_safe_closure( $token ); 216 | } 217 | 218 | sub access_token { 219 | my ($self) = @_; 220 | return $self->_access_token_closure->(); 221 | } 222 | 223 | =head2 private_token 224 | 225 | A GitLab API personal token. If set then L may not be set. 226 | 227 | See L. 228 | 229 | =cut 230 | 231 | has _private_token_arg => ( 232 | is => 'ro', 233 | isa => NonEmptySimpleStr, 234 | init_arg => 'private_token', 235 | clearer => '_clear_private_token_arg', 236 | ); 237 | 238 | has _private_token_closure => ( 239 | is => 'lazy', 240 | isa => CodeRef, 241 | init_arg => undef, 242 | builder => '_build_private_token_closure', 243 | ); 244 | sub _build_private_token_closure { 245 | my ($self) = @_; 246 | my $token = $self->_private_token_arg(); 247 | $self->_clear_private_token_arg(); 248 | return _make_safe_closure( $token ); 249 | } 250 | 251 | sub private_token { 252 | my ($self) = @_; 253 | return $self->_private_token_closure->(); 254 | } 255 | 256 | =head2 retries 257 | 258 | The number of times the request should be retried in case it fails (5XX HTTP 259 | response code). Defaults to C<0> (false), meaning that a failed request will 260 | not be retried. 261 | 262 | =cut 263 | 264 | has retries => ( 265 | is => 'ro', 266 | isa => PositiveOrZeroInt, 267 | default => 0, 268 | ); 269 | 270 | =head2 sudo_user 271 | 272 | The user to execute API calls as. You may find it more useful to use the 273 | L method instead. 274 | 275 | See L. 276 | 277 | =cut 278 | 279 | has sudo_user => ( 280 | is => 'ro', 281 | isa => NonEmptySimpleStr, 282 | ); 283 | 284 | =head2 rest_client 285 | 286 | An instance of L (or whatever L 287 | is set to). Typically you will not be setting this as it defaults to a new 288 | instance and customization should not be necessary. 289 | 290 | =cut 291 | 292 | has rest_client => ( 293 | is => 'lazy', 294 | isa => InstanceOf[ 'GitLab::API::v4::RESTClient' ], 295 | ); 296 | sub _build_rest_client { 297 | my ($self) = @_; 298 | 299 | return $self->rest_client_class->new( 300 | base_url => $self->url(), 301 | retries => $self->retries(), 302 | ); 303 | } 304 | 305 | =head2 rest_client_class 306 | 307 | The class to use when constructing the L. 308 | Defaults to L. 309 | 310 | =cut 311 | 312 | has rest_client_class => ( 313 | is => 'lazy', 314 | isa => NonEmptySimpleStr, 315 | ); 316 | sub _build_rest_client_class { 317 | return 'GitLab::API::v4::RESTClient'; 318 | } 319 | 320 | =head1 UTILITY METHODS 321 | 322 | =head2 paginator 323 | 324 | my $paginator = $api->paginator( $method, @method_args ); 325 | 326 | my $members = $api->paginator('group_members', $group_id); 327 | while (my $member = $members->next()) { 328 | ... 329 | } 330 | 331 | my $users_pager = $api->paginator('users'); 332 | while (my $users = $users_pager->next_page()) { 333 | ... 334 | } 335 | 336 | my $all_open_issues = $api->paginator( 337 | 'issues', 338 | $project_id, 339 | { state=>'opened' }, 340 | )->all(); 341 | 342 | Given a method who supports the C and C parameters, 343 | and returns an array ref, this will return a L 344 | object that will allow you to walk the records one page or one record 345 | at a time. 346 | 347 | =cut 348 | 349 | sub paginator { 350 | my ($self, $method, @args) = @_; 351 | 352 | my $params = (ref($args[-1]) eq 'HASH') ? pop(@args) : {}; 353 | 354 | return GitLab::API::v4::Paginator->new( 355 | api => $self, 356 | method => $method, 357 | args => \@args, 358 | params => $params, 359 | ); 360 | } 361 | 362 | =head2 sudo 363 | 364 | $api->sudo('fred')->create_issue(...); 365 | 366 | Returns a new instance of L with the L argument 367 | set. 368 | 369 | See L. 370 | 371 | =cut 372 | 373 | sub sudo { 374 | my ($self, $user) = @_; 375 | 376 | return $self->_clone( 377 | sudo_user => $user, 378 | ); 379 | } 380 | 381 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is copyright (c) 2014 by Aran Clary Deltac . 2 | 3 | This is free software; you can redistribute it and/or modify it under 4 | the same terms as the Perl 5 programming language system itself. 5 | 6 | Terms of the Perl programming language system itself 7 | 8 | a) the GNU General Public License as published by the Free 9 | Software Foundation; either version 1, or (at your option) any 10 | later version, or 11 | b) the "Artistic License" 12 | 13 | --- The GNU General Public License, Version 1, February 1989 --- 14 | 15 | This software is Copyright (c) 2019 by Aran Clary Deltac . 16 | 17 | This is free software, licensed under: 18 | 19 | The GNU General Public License, Version 1, February 1989 20 | 21 | GNU GENERAL PUBLIC LICENSE 22 | Version 1, February 1989 23 | 24 | Copyright (C) 1989 Free Software Foundation, Inc. 25 | 51 Franklin St, Suite 500, Boston, MA 02110-1335 USA 26 | 27 | Everyone is permitted to copy and distribute verbatim copies 28 | of this license document, but changing it is not allowed. 29 | 30 | Preamble 31 | 32 | The license agreements of most software companies try to keep users 33 | at the mercy of those companies. By contrast, our General Public 34 | License is intended to guarantee your freedom to share and change free 35 | software--to make sure the software is free for all its users. The 36 | General Public License applies to the Free Software Foundation's 37 | software and to any other program whose authors commit to using it. 38 | You can use it for your programs, too. 39 | 40 | When we speak of free software, we are referring to freedom, not 41 | price. Specifically, the General Public License is designed to make 42 | sure that you have the freedom to give away or sell copies of free 43 | software, that you receive source code or can get it if you want it, 44 | that you can change the software or use pieces of it in new free 45 | programs; and that you know you can do these things. 46 | 47 | To protect your rights, we need to make restrictions that forbid 48 | anyone to deny you these rights or to ask you to surrender the rights. 49 | These restrictions translate to certain responsibilities for you if you 50 | distribute copies of the software, or if you modify it. 51 | 52 | For example, if you distribute copies of a such a program, whether 53 | gratis or for a fee, you must give the recipients all the rights that 54 | you have. You must make sure that they, too, receive or can get the 55 | source code. And you must tell them their rights. 56 | 57 | We protect your rights with two steps: (1) copyright the software, and 58 | (2) offer you this license which gives you legal permission to copy, 59 | distribute and/or modify the software. 60 | 61 | Also, for each author's protection and ours, we want to make certain 62 | that everyone understands that there is no warranty for this free 63 | software. If the software is modified by someone else and passed on, we 64 | want its recipients to know that what they have is not the original, so 65 | that any problems introduced by others will not reflect on the original 66 | authors' reputations. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | GNU GENERAL PUBLIC LICENSE 72 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 73 | 74 | 0. This License Agreement applies to any program or other work which 75 | contains a notice placed by the copyright holder saying it may be 76 | distributed under the terms of this General Public License. The 77 | "Program", below, refers to any such program or work, and a "work based 78 | on the Program" means either the Program or any work containing the 79 | Program or a portion of it, either verbatim or with modifications. Each 80 | licensee is addressed as "you". 81 | 82 | 1. You may copy and distribute verbatim copies of the Program's source 83 | code as you receive it, in any medium, provided that you conspicuously and 84 | appropriately publish on each copy an appropriate copyright notice and 85 | disclaimer of warranty; keep intact all the notices that refer to this 86 | General Public License and to the absence of any warranty; and give any 87 | other recipients of the Program a copy of this General Public License 88 | along with the Program. You may charge a fee for the physical act of 89 | transferring a copy. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion of 92 | it, and copy and distribute such modifications under the terms of Paragraph 93 | 1 above, provided that you also do the following: 94 | 95 | a) cause the modified files to carry prominent notices stating that 96 | you changed the files and the date of any change; and 97 | 98 | b) cause the whole of any work that you distribute or publish, that 99 | in whole or in part contains the Program or any part thereof, either 100 | with or without modifications, to be licensed at no charge to all 101 | third parties under the terms of this General Public License (except 102 | that you may choose to grant warranty protection to some or all 103 | third parties, at your option). 104 | 105 | c) If the modified program normally reads commands interactively when 106 | run, you must cause it, when started running for such interactive use 107 | in the simplest and most usual way, to print or display an 108 | announcement including an appropriate copyright notice and a notice 109 | that there is no warranty (or else, saying that you provide a 110 | warranty) and that users may redistribute the program under these 111 | conditions, and telling the user how to view a copy of this General 112 | Public License. 113 | 114 | d) You may charge a fee for the physical act of transferring a 115 | copy, and you may at your option offer warranty protection in 116 | exchange for a fee. 117 | 118 | Mere aggregation of another independent work with the Program (or its 119 | derivative) on a volume of a storage or distribution medium does not bring 120 | the other work under the scope of these terms. 121 | 122 | 3. You may copy and distribute the Program (or a portion or derivative of 123 | it, under Paragraph 2) in object code or executable form under the terms of 124 | Paragraphs 1 and 2 above provided that you also do one of the following: 125 | 126 | a) accompany it with the complete corresponding machine-readable 127 | source code, which must be distributed under the terms of 128 | Paragraphs 1 and 2 above; or, 129 | 130 | b) accompany it with a written offer, valid for at least three 131 | years, to give any third party free (except for a nominal charge 132 | for the cost of distribution) a complete machine-readable copy of the 133 | corresponding source code, to be distributed under the terms of 134 | Paragraphs 1 and 2 above; or, 135 | 136 | c) accompany it with the information you received as to where the 137 | corresponding source code may be obtained. (This alternative is 138 | allowed only for noncommercial distribution and only if you 139 | received the program in object code or executable form alone.) 140 | 141 | Source code for a work means the preferred form of the work for making 142 | modifications to it. For an executable file, complete source code means 143 | all the source code for all modules it contains; but, as a special 144 | exception, it need not include source code for modules which are standard 145 | libraries that accompany the operating system on which the executable 146 | file runs, or for standard header files or definitions files that 147 | accompany that operating system. 148 | 149 | 4. You may not copy, modify, sublicense, distribute or transfer the 150 | Program except as expressly provided under this General Public License. 151 | Any attempt otherwise to copy, modify, sublicense, distribute or transfer 152 | the Program is void, and will automatically terminate your rights to use 153 | the Program under this License. However, parties who have received 154 | copies, or rights to use copies, from you under this General Public 155 | License will not have their licenses terminated so long as such parties 156 | remain in full compliance. 157 | 158 | 5. By copying, distributing or modifying the Program (or any work based 159 | on the Program) you indicate your acceptance of this license to do so, 160 | and all its terms and conditions. 161 | 162 | 6. Each time you redistribute the Program (or any work based on the 163 | Program), the recipient automatically receives a license from the original 164 | licensor to copy, distribute or modify the Program subject to these 165 | terms and conditions. You may not impose any further restrictions on the 166 | recipients' exercise of the rights granted herein. 167 | 168 | 7. The Free Software Foundation may publish revised and/or new versions 169 | of the General Public License from time to time. Such new versions will 170 | be similar in spirit to the present version, but may differ in detail to 171 | address new problems or concerns. 172 | 173 | Each version is given a distinguishing version number. If the Program 174 | specifies a version number of the license which applies to it and "any 175 | later version", you have the option of following the terms and conditions 176 | either of that version or of any later version published by the Free 177 | Software Foundation. If the Program does not specify a version number of 178 | the license, you may choose any version ever published by the Free Software 179 | Foundation. 180 | 181 | 8. If you wish to incorporate parts of the Program into other free 182 | programs whose distribution conditions are different, write to the author 183 | to ask for permission. For software which is copyrighted by the Free 184 | Software Foundation, write to the Free Software Foundation; we sometimes 185 | make exceptions for this. Our decision will be guided by the two goals 186 | of preserving the free status of all derivatives of our free software and 187 | of promoting the sharing and reuse of software generally. 188 | 189 | NO WARRANTY 190 | 191 | 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 192 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 193 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 194 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 195 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 196 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 197 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 198 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 199 | REPAIR OR CORRECTION. 200 | 201 | 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 202 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 203 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 204 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 205 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 206 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 207 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 208 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 209 | POSSIBILITY OF SUCH DAMAGES. 210 | 211 | END OF TERMS AND CONDITIONS 212 | 213 | Appendix: How to Apply These Terms to Your New Programs 214 | 215 | If you develop a new program, and you want it to be of the greatest 216 | possible use to humanity, the best way to achieve this is to make it 217 | free software which everyone can redistribute and change under these 218 | terms. 219 | 220 | To do so, attach the following notices to the program. It is safest to 221 | attach them to the start of each source file to most effectively convey 222 | the exclusion of warranty; and each file should have at least the 223 | "copyright" line and a pointer to where the full notice is found. 224 | 225 | 226 | Copyright (C) 19yy 227 | 228 | This program is free software; you can redistribute it and/or modify 229 | it under the terms of the GNU General Public License as published by 230 | the Free Software Foundation; either version 1, or (at your option) 231 | any later version. 232 | 233 | This program is distributed in the hope that it will be useful, 234 | but WITHOUT ANY WARRANTY; without even the implied warranty of 235 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 236 | GNU General Public License for more details. 237 | 238 | You should have received a copy of the GNU General Public License 239 | along with this program; if not, write to the Free Software 240 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA 241 | 242 | 243 | Also add information on how to contact you by electronic and paper mail. 244 | 245 | If the program is interactive, make it output a short notice like this 246 | when it starts in an interactive mode: 247 | 248 | Gnomovision version 69, Copyright (C) 19xx name of author 249 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 250 | This is free software, and you are welcome to redistribute it 251 | under certain conditions; type `show c' for details. 252 | 253 | The hypothetical commands `show w' and `show c' should show the 254 | appropriate parts of the General Public License. Of course, the 255 | commands you use may be called something other than `show w' and `show 256 | c'; they could even be mouse-clicks or menu items--whatever suits your 257 | program. 258 | 259 | You should also get your employer (if you work as a programmer) or your 260 | school, if any, to sign a "copyright disclaimer" for the program, if 261 | necessary. Here a sample; alter the names: 262 | 263 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 264 | program `Gnomovision' (a program to direct compilers to make passes 265 | at assemblers) written by James Hacker. 266 | 267 | , 1 April 1989 268 | Ty Coon, President of Vice 269 | 270 | That's all there is to it! 271 | 272 | 273 | --- The Artistic License 1.0 --- 274 | 275 | This software is Copyright (c) 2019 by Aran Clary Deltac . 276 | 277 | This is free software, licensed under: 278 | 279 | The Artistic License 1.0 280 | 281 | The Artistic License 282 | 283 | Preamble 284 | 285 | The intent of this document is to state the conditions under which a Package 286 | may be copied, such that the Copyright Holder maintains some semblance of 287 | artistic control over the development of the package, while giving the users of 288 | the package the right to use and distribute the Package in a more-or-less 289 | customary fashion, plus the right to make reasonable modifications. 290 | 291 | Definitions: 292 | 293 | - "Package" refers to the collection of files distributed by the Copyright 294 | Holder, and derivatives of that collection of files created through 295 | textual modification. 296 | - "Standard Version" refers to such a Package if it has not been modified, 297 | or has been modified in accordance with the wishes of the Copyright 298 | Holder. 299 | - "Copyright Holder" is whoever is named in the copyright or copyrights for 300 | the package. 301 | - "You" is you, if you're thinking about copying or distributing this Package. 302 | - "Reasonable copying fee" is whatever you can justify on the basis of media 303 | cost, duplication charges, time of people involved, and so on. (You will 304 | not be required to justify it to the Copyright Holder, but only to the 305 | computing community at large as a market that must bear the fee.) 306 | - "Freely Available" means that no fee is charged for the item itself, though 307 | there may be fees involved in handling the item. It also means that 308 | recipients of the item may redistribute it under the same conditions they 309 | received it. 310 | 311 | 1. You may make and give away verbatim copies of the source form of the 312 | Standard Version of this Package without restriction, provided that you 313 | duplicate all of the original copyright notices and associated disclaimers. 314 | 315 | 2. You may apply bug fixes, portability fixes and other modifications derived 316 | from the Public Domain or from the Copyright Holder. A Package modified in such 317 | a way shall still be considered the Standard Version. 318 | 319 | 3. You may otherwise modify your copy of this Package in any way, provided that 320 | you insert a prominent notice in each changed file stating how and when you 321 | changed that file, and provided that you do at least ONE of the following: 322 | 323 | a) place your modifications in the Public Domain or otherwise make them 324 | Freely Available, such as by posting said modifications to Usenet or an 325 | equivalent medium, or placing the modifications on a major archive site 326 | such as ftp.uu.net, or by allowing the Copyright Holder to include your 327 | modifications in the Standard Version of the Package. 328 | 329 | b) use the modified Package only within your corporation or organization. 330 | 331 | c) rename any non-standard executables so the names do not conflict with 332 | standard executables, which must also be provided, and provide a separate 333 | manual page for each non-standard executable that clearly documents how it 334 | differs from the Standard Version. 335 | 336 | d) make other distribution arrangements with the Copyright Holder. 337 | 338 | 4. You may distribute the programs of this Package in object code or executable 339 | form, provided that you do at least ONE of the following: 340 | 341 | a) distribute a Standard Version of the executables and library files, 342 | together with instructions (in the manual page or equivalent) on where to 343 | get the Standard Version. 344 | 345 | b) accompany the distribution with the machine-readable source of the Package 346 | with your modifications. 347 | 348 | c) accompany any non-standard executables with their corresponding Standard 349 | Version executables, giving the non-standard executables non-standard 350 | names, and clearly documenting the differences in manual pages (or 351 | equivalent), together with instructions on where to get the Standard 352 | Version. 353 | 354 | d) make other distribution arrangements with the Copyright Holder. 355 | 356 | 5. You may charge a reasonable copying fee for any distribution of this 357 | Package. You may charge any fee you choose for support of this Package. You 358 | may not charge a fee for this Package itself. However, you may distribute this 359 | Package in aggregate with other (possibly commercial) programs as part of a 360 | larger (possibly commercial) software distribution provided that you do not 361 | advertise this Package as a product of your own. 362 | 363 | 6. The scripts and library files supplied as input to or produced as output 364 | from the programs of this Package do not automatically fall under the copyright 365 | of this Package, but belong to whomever generated them, and may be sold 366 | commercially, and may be aggregated with this Package. 367 | 368 | 7. C or perl subroutines supplied by you and linked into this Package shall not 369 | be considered part of this Package. 370 | 371 | 8. The name of the Copyright Holder may not be used to endorse or promote 372 | products derived from this software without specific prior written permission. 373 | 374 | 9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED 375 | WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF 376 | MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. 377 | 378 | The End 379 | --------------------------------------------------------------------------------