Skip to content

Latest commit

 

History

History
1242 lines (1013 loc) · 37.9 KB

jkd.org

File metadata and controls

1242 lines (1013 loc) · 37.9 KB

JKD is Jira Keeps me Doing stuff, not Jeet Kune Do

This one is for using Jira on the command line, maybe also together with Emacs.

I still need a way to work with Jira, org-jira just can’t fulfill my needs anymore.

The dispatcher (copied from org-kungfu)

Which sub command to call?

This is simple using Perl’s “reflection”.

$ENV{SELECT_HISTORY_ORDER} = 'jkd';


(my $sub_command = "jkd_" . shift) =~ s,-,_,g;

$ENV{PERL5LIB} = join(":", "$ENV{scm_common_libdir}/jkd.pm", $ENV{PERL5LIB});

if (not defined &$sub_command) {
    my $lib_command;

    for ("jkd/", "jkd.user/") {
        ($lib_command = $sub_command) =~ s,^jkd_,$_,;
        $lib_command =~ s,_,-,g;

        $lib_command = "$ENV{scm_common_libdir}/$lib_command";

        if (-x $lib_command) {
            do {
                use IPC::System::Simple qw(run runx capture capturex $EXITVAL EXIT_ANY);
                my @args = ("$lib_command", @ARGV);
                if ($ENV{JKD_TRACE} eq 'true') {
                    @args = ('debug-run', @args);
                }
                die "Can't run jkd " . join(" ", shell_quote(@args)) if runx(EXIT_ANY, @args) != 0;
                exit;
            }
        }
    }

    &$handler_help(1, "Can't find sub-command: $lib_command");
}

$sub_command = \&{$sub_command};
&$sub_command(@ARGV);

help for sub commands

sub subcmd_help() {
    my $top_help_str = <<~'EOF';
        Usage: jkd [GLOBAL_OPTIONS]... SUB_COMMAND SUBCMD_ARGS...
        Here's the list of sub-commands:
        EOF

    my @subcmd_help_strs;
    my %subcmd_helpstr_map = (
        e => "Edit jira issue in emacs org-mode",
        tri => "Transition of an issue",
        );

    my %help_printed_map;

    for my $subcmd ((sort {$a cmp $b} grep {m/^jkd_/} keys %::), (sort {$a cmp $b} keys %subcmd_helpstr_map)) {
        (my $raw_subcmd = $subcmd) =~ s,^jkd_,,;
        $subcmd = "jkd_$raw_subcmd";
        if ($help_printed_map{$raw_subcmd}) {
            next;
        } else {
            $help_printed_map{$raw_subcmd} = 1;
        }

        my $subcmd_help_str = $subcmd_helpstr_map{$raw_subcmd} ||
            "NO DESCRIPTION.";

        if (not defined &$subcmd) {
            $subcmd_help_str .= " (NO DEFINITION)"
        }

        push @subcmd_help_strs, sprintf("    %s\n\t%s", $raw_subcmd, $subcmd_help_str);
    }

    return join "\n", $top_help_str, @subcmd_help_strs;
}

The final script

# Local Variables: #
# eval: (read-only-mode 1) #
# End: #
#!/usr/bin/env bash

# Given a page, I will edit this
#!/usr/bin/env perl
use strict;
use v5.10.1; # for say and switch
use autodie qw(:all);
use IPC::System::Simple qw(run runx capture capturex $EXITVAL EXIT_ANY);
binmode(STDOUT, ":utf8");
binmode(STDERR, ":utf8");
use Encode;
use utf8;
@ARGV = map {decode_utf8 $_} @ARGV;

use JSON;

my $json = JSON->new->utf8->canonical->pretty;

BEGIN {
    push @INC, "$ENV{scm_common_libdir}/jkd.pm";
}
use jkd;

## start code-generator "^\\s *#\\s *"
# generate-getopt -P -s perl -p jkd \
# '?subcmd_help()' \
# u:username='"$ENV{scm_jira_user}"' '?"Login Username"' \
# p:password='"$ENV{scm_jira_password}"' '?"Login Password"' \
# j:jiraurl='"$ENV{scm_jira_url}"' '?"Jira URL (only FQDN, no / and such)"' \
# vverbose='"$ENV{jkd_verbose}"' '?"Verbose debug output"'
## end code-generator
## start generated code
use Getopt::Long;

Getopt::Long::Configure("posix_default");



my $jkd_jiraurl = "$ENV{scm_jira_url}";
my $jkd_password = "$ENV{scm_jira_password}";
my $jkd_username = "$ENV{scm_jira_user}";
my $jkd_verbose = "$ENV{jkd_verbose}";

my $handler_help = sub {
    print subcmd_help();
    print "\n\n选项和参数:\n";
    printf "%6s", '-j, ';
    printf "%-24s", '--jiraurl=JIRAURL';
    if (length('--jiraurl=JIRAURL') > 24 and length("Jira URL (only FQDN, no / and such)") > 0) {
        print "\n";
        printf "%30s", "";
    }
    printf "%s", "Jira URL (only FQDN, no / and such)";
    print "\n";
    printf "%6s", '-p, ';
    printf "%-24s", '--password=PASSWORD';
    if (length('--password=PASSWORD') > 24 and length("Login Password") > 0) {
        print "\n";
        printf "%30s", "";
    }
    printf "%s", "Login Password";
    print "\n";
    printf "%6s", '-u, ';
    printf "%-24s", '--username=USERNAME';
    if (length('--username=USERNAME') > 24 and length("Login Username") > 0) {
        print "\n";
        printf "%30s", "";
    }
    printf "%s", "Login Username";
    print "\n";
    printf "%6s", '-v, ';
    printf "%-24s", '--[no]verbose';
    if (length('--[no]verbose') > 24 and length("Verbose debug output") > 0) {
        print "\n";
        printf "%30s", "";
    }
    printf "%s", "Verbose debug output";
    print "\n";

    my $exit_value = 0;
    if (@_ && $_[0] ne "help" && $_[1] != 1) {
        $exit_value = shift @_;
        print "@_\n";
    }

    exit($exit_value);
};

GetOptions (
    'jiraurl|j=s' => \$jkd_jiraurl,
    'password|p=s' => \$jkd_password,
    'username|u=s' => \$jkd_username,
    'verbose|v!' => \$jkd_verbose,
    'help|h!' => \&$handler_help,
);


## end generated code
use v5.10;
use String::ShellQuote;

if ($jkd_verbose) {
    say STDERR  "jkd ", shell_quote(@ARGV);
}

my $secret_conf;
use Config::GitLike;

my ($config_file) = $ENV{scm_secrets_conf};

if (-e $config_file) {
    $secret_conf = Config::GitLike->load_file($config_file);
}

if (not $jkd_password) {
    $jkd_password = $secret_conf->{"ldap.${jkd_username}.password"};
}

if (not $jkd_password) {
    &$handler_help(1, "Must specify the jira password")
}

if ($jkd_jiraurl =~ m/^\w+$/) {
    my $new_jkd_jiraurl = $ENV{"scm_jira_${jkd_jiraurl}_url"};
    die "Can't find jira url scm_jira_${jkd_jiraurl}_url from env" unless $new_jkd_jiraurl;
    $jkd_jiraurl = $new_jkd_jiraurl;
}

$ENV{scm_jira_url} = $jkd_jiraurl;
$ENV{scm_jira_user} = $jkd_username; # for lib scripts
$ENV{scm_jira_password} = $jkd_password;
$ENV{jkd_verbose} = $jkd_verbose;

if (not $jkd_username) {
    $jkd_username = $secret_conf->{"jkd.username"};
}

if (not $jkd_username) {
    say STDERR  "Must specify the jira username";
    &$handler_help(1);
}

if (not $jkd_jiraurl) {
    $jkd_jiraurl = $secret_conf->{"jkd.jiraurl"};
}

if (not $jkd_jiraurl) {
    say STDERR  "Must specify the jira url";
    &$handler_help(1);
}

use File::Path;
<<global-args>>
<<create-1-issue>>
<<comment-issue>>
<<mv-issue-to-sprint>>
<<transition-1-issue>>
<<subcmd-helps>>
<<which-to-call>>
<<read-only>>
# Local Variables: #
# eval: (read-only-mode 1) #
# End: #

create 1 issue

use v5.10;
use HTTP::Request::Common;
use LWP::UserAgent;
use JSON;
use File::Path qw(make_path);
use File::Basename;
use Encode;

sub jkd_url_for_api($) {
    (my $api_path = $_[0]) =~ s,^/,,;

    my $auth_str = sprintf "%s:%s@", $jkd_username, $jkd_password;
    (my $scm_jira_site = $jkd_jiraurl) =~ s,(https?://),$1$auth_str,;
    my $url = "${scm_jira_site}${api_path}";
    if ($jkd_verbose) {
        say STDERR "api: $url";
    }

    return "$url";
}

sub get($) {
    my $ua = LWP::UserAgent->new;
    my $api = $_[0];
    my $url = jkd_url_for_api($api);

    for (1..3) {
        my $response = $ua->request(GET $url);
        if ($response->code != 200) {
            die "Can't get $api: code is " . $response->code . ", url is $url";
        }

        if ($response->content eq "") {
            say STDERR "empty response for $api? try: $_";
            sleep($_ * $_);
            next unless $_ == 3;
        }
        return $response;
    }
}


sub jkd_get(@) {
    ## start code-generator "^\\s *#\\s *"
    # generate-getopt -s perl -P a:api '?"for e.g., rest/api/2/project/"'
    ## end code-generator
    ## start generated code
    use Getopt::Long;

    Getopt::Long::Configure("posix_default");



    my $api = "";

    my $handler_help = sub {
        print ;
        print "\n\n选项和参数:\n";
        printf "%6s", '-a, ';
        printf "%-24s", '--api=API';
        if (length('--api=API') > 24 and length("for e.g., rest/api/2/project/") > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", "for e.g., rest/api/2/project/";
        print "\n";

        exit(0);
    };

    GetOptions (
        'api|a=s' => \$api,
        'help|h!' => \&$handler_help,
    );


    ## end generated code

    if (not $api) {
        $api = $ARGV[0];
    }

    if ($api !~ m,rest/api/,) {
        ($api = "rest/api/2/$api") =~ s,/+,/,g;
    }

    if (not $api) {
        die "Must specify the api with -a API";
    }

        my $response = get($api);
        print decode_utf8($response->content);
}

sub jkd_get_issue_type_fields(@) {
    ## start code-generator "^\\s *#\\s *"
    # generate-getopt -s perl -l p:project t:issue-type vverbose '?"print the json"'
    ## end code-generator
    ## start generated code
    use Getopt::Long;

    Getopt::Long::Configure("default");

    local @ARGV = @_;

    my $issue_type = "";
    my $project = "";
    my $verbose = 0;

    my $handler_help = sub {
        print ;
        print "\n\n选项和参数:\n";
        printf "%6s", '-t, ';
        printf "%-24s", '--issue-type=ISSUE-TYPE';
        if (length('--issue-type=ISSUE-TYPE') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-p, ';
        printf "%-24s", '--project=PROJECT';
        if (length('--project=PROJECT') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-v, ';
        printf "%-24s", '--[no]verbose';
        if (length('--[no]verbose') > 24 and length("print the json") > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", "print the json";
        print "\n";

        exit(0);
    };

    GetOptions (
        'issue-type|t=s' => \$issue_type,
        'project|p=s' => \$project,
        'verbose|v!' => \$verbose,
        'help|h!' => \&$handler_help,
        );


    ## end generated code

    if (not $issue_type or not $issue_type =~ m/^\d+$/) {
        $issue_type = jkd_select_issue_type("-p", "$project", "-t", $issue_type);
    }

    my $issue_fields_resp = get("rest/api/2/issue/createmeta?projectKeys=${project}&issuetypeIds=${issue_type}&expand=projects.issuetypes.fields");

    print $issue_fields_resp->content if $verbose;
    return decode_json $issue_fields_resp->content;
}

use File::Slurp;

sub org_to_jira($) {
    my $text = shell_quote($_[0]);
    return scalar decode_utf8(qx(ejwo --o2j --text $text </dev/null));
}

sub jira_to_org($) {
    my $text = shell_quote($_[0]);
    return scalar decode_utf8(qx(ejwo --j2o --text $text </dev/null));
}

sub work_with_all_fields($$\%$) {
    my ($project_id, $issue_type_id, $required_fields, $work_options) = @_;
    my $edit_issue_json_obj = $work_options->{edit_issue_json_obj};
    my $print_schemes = $work_options->{"print-schemes"};
    my $issue_fields_obj = jkd_get_issue_type_fields("-p", "$project_id", "-t", "$issue_type_id");


    for my $project (@{$issue_fields_obj->{projects}}) {
        for my $it (@{$project->{issuetypes}}) {
            if ($it->{id} != $issue_type_id) {
                next;
            }

            my @fields_to_edit;
            if ($edit_issue_json_obj) {
                my @command = (
                    "select-args-n", "-p", "请输入你想要编辑的域",
                    map {
                        sprintf "%s: %s", $_, $it->{fields}{$_}{name}
                    }
                    grep {
                        $it->{fields}{$_}{required}
                    } sort keys %{$it->{fields}}
                    );
                my $command = join(" ", shell_quote(@command));
                my $values = decode_utf8 qx($command);
                $values =~ s,:.*,,mg;
                @fields_to_edit = split(" ", $values);
                say STDERR "fields_to_edit is @fields_to_edit";
            } else {
                @fields_to_edit = sort keys %{$it->{fields}};
            }

            $required_fields->{assignee} = {name => $ENV{scm_jira_user}} if exists $it->{fields}{assignee} and not $edit_issue_json_obj;

            for my $field_key (@fields_to_edit) {
                my $field_name = ($it->{fields}{$field_key}{name} or "$field_key (field has no name)");

                say STDERR "field ${field_name}'s value is ", ($edit_issue_json_obj->{fields}{$field_key} || "");

                if ($print_schemes) {
                    print "--field-value $field_name= ";
                    next;
                }

                if ($required_fields->{$field_name}) {
                    $required_fields->{$field_key} = $required_fields->{$field_name};
                    delete $required_fields->{$field_name} unless ${field_key} eq ${field_name};
                    next;
                }
                if ($it->{fields}{$field_key}{required}) {
                    if ($field_key eq 'project' || $field_key eq 'issuetype') {
                        next;
                    }

                    my $schema_type = $it->{fields}{$field_key}{schema}{type};

                    my %selection_types = (
                        array => 1,
                        option => 1,
                    );


                    if ($schema_type eq 'string') {
                        my $init_text = ($edit_issue_json_obj->{fields}{$field_key} || $required_fields->{$field_key});
                        if ($field_key eq "description") {
                            say STDERR "special treatment for description";
                            $init_text = jira_to_org $init_text;
                          }

                        my @command = (
                            "ask-for-input-with-emacs", "-p", sprintf("Please input the %s (field key: %s)", $field_name, $field_key),
                            "--init-text", $init_text
                            );
                        my $command = join(" ", shell_quote(@command));
                        say STDERR "command is $command";

                        my $result_text = decode_utf8 qx($command);
                        if ($field_key eq "description") {
                            $result_text = org_to_jira $result_text;
                        }

                        $required_fields->{$field_key} = $result_text;
                    } elsif ($selection_types{$schema_type}) {
                        my %allowed_values_map;
                        map {
                            my $key = $_->{value} || $_->{name};
                            $allowed_values_map{$key} = $_->{id}} @{$it->{fields}{$field_key}{allowedValues}};

                        my $select_command;

                        if ($schema_type eq "array") {
                            $select_command = "select-args-n";
                        } else {
                            $select_command = "select-args";
                        }
                        my @command = (
                            $select_command, "-p", ("请输入你想要选择的 " . "$field_name"),
                            keys %allowed_values_map
                            );
                        my $command = join(" ", shell_quote(@command));
                        my $values = decode_utf8 qx($command);
                        $required_fields->{$field_key} = [] if $schema_type eq "array";
                        for (split "\n", $values) {
                            next unless $_;
                            say "Adding option for $field_name: ", $_;
                            die "invalid $_" unless ${allowed_values_map{$_}};
                            if ($schema_type eq "array") {
                                push @{$required_fields->{$field_key}}, {id => $allowed_values_map{$_}};
                            } else {
                                $required_fields->{$field_key} = {id => $allowed_values_map{$_}};
                            }

                        }
                    } else {
                        my $jsonParser = JSON->new->allow_nonref;
                        my $jsonText = decode_utf8($json->encode($it->{fields}{$field_key}));
                        say <<EOF;

$jsonText

Don't know how to deal with ${field_name}, please input with json.

EOF
                        if ($field_key eq "reporter") {
                            $required_fields->{$field_key} = "$ENV{scm_jira_user}";
                        } else {
                            while (1) {
                                $required_fields->{$field_key} = eval 'decode_json(qx(ask-for-input -p "what is your input json for $field_key?"))';
                                last unless $@;
                            }
                        }
                    }
                }
            }
        }
    }
    if ($print_schemes) {
        exit;
    }
}

sub get_issue_type($$$) {
    my ($projects_issuetypes, $project_id, $issue_type_id) = @_;
    for (@{$projects_issuetypes->{projects}}) {
        if ($_->{id} == $project_id || $_->{key} eq $project_id) {
            for (@{$_->{issuetypes}}) {
                if ($_->{id} == $issue_type_id) {
                    return $_;
                }
            }
        }
    }
}

sub jkd_e(@) {
    ## start code-generator "^\\s *#\\s *"
    # generate-getopt -s perl i:issue-to-edit f:field-to-edit @:fields-json='"{}"'
    ## end code-generator
    ## start generated code
    use Getopt::Long;

    Getopt::Long::Configure("default");



    my $field_to_edit = "";
    my $fields_json = "{}";
    my $issue_to_edit = "";

    my $handler_help = sub {
        print ;
        print "\n\n选项和参数:\n";
        printf "%6s", '-f, ';
        printf "%-24s", '--field-to-edit=FIELD-TO-EDIT';
        if (length('--field-to-edit=FIELD-TO-EDIT') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '';
        printf "%-24s", '--fields-json=FIELDS-JSON';
        if (length('--fields-json=FIELDS-JSON') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-i, ';
        printf "%-24s", '--issue-to-edit=ISSUE-TO-EDIT';
        if (length('--issue-to-edit=ISSUE-TO-EDIT') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";

        exit(0);
    };

    GetOptions (
        'field-to-edit|f=s' => \$field_to_edit,
        'fields-json=s' => \$fields_json,
        'issue-to-edit|i=s' => \$issue_to_edit,
        'help|h!' => \&$handler_help,
    );


    ## end generated code

    if (not $issue_to_edit) {
        die "You must specify -issue-to-edit";
    }

    my $json_issue = decode_json get("rest/api/2/issue/$issue_to_edit")->content;
    my $issue_type_id = $json_issue->{fields}{issuetype}{id};
    my $issue_project = $json_issue->{fields}{project}{key};
    my $issue_fields_obj = jkd_get_issue_type_fields("-p", "$issue_project", "-t", "$issue_type_id");

    my %edited_fields;

    if ($fields_json eq "{}") {
        work_with_all_fields (
            $issue_project, $issue_type_id, %edited_fields,
            {
                edit_issue_json_obj => $json_issue,
            }
        );
    } else {
        my $issue_fields_obj = decode_json get("rest/api/2/issue/${issue_to_edit}?expand=names")->content;
        my $fields_json_obj = decode_json(encode_utf8($fields_json));
        update_names_with_fields($fields_json_obj, $issue_fields_obj->{names});
        %edited_fields = %$fields_json_obj;
    }


    my $ua = LWP::UserAgent->new;

    for my $try (1..3) {
        my $request = PUT jkd_url_for_api("rest/api/2/issue/$issue_to_edit"),
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            "charset" => "utf-8",
            Content => encode_json {
                fields => \%edited_fields
            };
        my $response = $ua->request($request);

        say "PUT \@${try} response code:" . $response->code, "result: ", decode_utf8($response->content);

        last if $response->is_success;

        my $errors = decode_json($response->content)->{errors};
        for (keys %$errors) {
            # die "Can't find $_" unless $required_fields{$_};
            say STDERR "Delete $_ and try again";
            delete $edited_fields{$_};
        }
    }
}

sub jkd_resolve(@) {
    ## start code-generator "^\\s *#\\s *"
    # generate-getopt -s perl -P i:issue-to-edit r:resolution
    ## end code-generator
    ## start generated code
    use Getopt::Long;

    Getopt::Long::Configure("posix_default");



    my $issue_to_edit = "";
    my $resolution = "";

    my $handler_help = sub {
        print ;
        print "\n\n选项和参数:\n";
        printf "%6s", '-i, ';
        printf "%-24s", '--issue-to-edit=ISSUE-TO-EDIT';
        if (length('--issue-to-edit=ISSUE-TO-EDIT') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-r, ';
        printf "%-24s", '--resolution=RESOLUTION';
        if (length('--resolution=RESOLUTION') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";

        exit(0);
    };

    GetOptions (
        'issue-to-edit|i=s' => \$issue_to_edit,
        'resolution|r=s' => \$resolution,
        'help|h!' => \&$handler_help,
    );


    ## end generated code

    my $ua = LWP::UserAgent->new;

    my $request = PUT jkd_url_for_api("rest/api/2/issue/$issue_to_edit"),
        'Content-Type' => 'application/json',
        'Accept' => 'application/json',
        "charset" => "utf-8",
        Content => encode_json {
            fields => {
                resolution => {
                    id => 10300
                }
            }
        };
    my $response = $ua->request($request);

    say "PUT response code:" . $response->code, "result: ", decode_utf8($response->content);
}

sub jkd_c(@) { # create issue
    if ($ENV{JKD_TRACE} eq "true") {
        runx("debug-run", "log", "jkd", "c", @_);
    }

    ## start code-generator "^\\s *#\\s *"
    # generate-getopt -s perl -l \
    #     p:project \
    #     t:issue-type '?"指定要创建的 issue 类型,比如 bug、feature、story 等(取决于 project)"' \
    #     @assign-to-myself=1 \
    #     @:field-value='()' '?"可指定多次。格式为简单的 name=value。不支持复杂的数据"' \
    #     @print-schemes \
    #     @:fields-json
    ## end code-generator
    ## start generated code
    use Getopt::Long;

    Getopt::Long::Configure("default");

    local @ARGV = @_;

    my $assign_to_myself = 1;
    my @field_value = ();
    my $fields_json = "";
    my $issue_type = "";
    my $print_schemes = 0;
    my $project = "";

    my $handler_help = sub {
        print ;
        print "\n\n选项和参数:\n";
        printf "%6s", '';
        printf "%-24s", '--[no]assign-to-myself';
        if (length('--[no]assign-to-myself') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '';
        printf "%-24s", '--field-value=FIELD-VALUE';
        if (length('--field-value=FIELD-VALUE') > 24 and length("可指定多次。格式为简单的 name=value。不支持复杂的数据") > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", "可指定多次。格式为简单的 name=value。不支持复杂的数据";
        print "\n";
        printf "%6s", '';
        printf "%-24s", '--fields-json=FIELDS-JSON';
        if (length('--fields-json=FIELDS-JSON') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-t, ';
        printf "%-24s", '--issue-type=ISSUE-TYPE';
        if (length('--issue-type=ISSUE-TYPE') > 24 and length("指定要创建的 issue 类型,比如 bug、feature、story 等(取决于 project)") > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", "指定要创建的 issue 类型,比如 bug、feature、story 等(取决于 project)";
        print "\n";
        printf "%6s", '';
        printf "%-24s", '--[no]print-schemes';
        if (length('--[no]print-schemes') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-p, ';
        printf "%-24s", '--project=PROJECT';
        if (length('--project=PROJECT') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";

        my $exit_value = 0;
        if (@_ && $_[0] ne "help" && $_[1] != 1) {
            $exit_value = shift @_;
            print "@_\n";
        }

        exit($exit_value);
    };

    GetOptions (
        'assign-to-myself!' => \$assign_to_myself,
        'field-value=s' => \@field_value,
        'fields-json=s' => \$fields_json,
        'issue-type|t=s' => \$issue_type,
        'print-schemes!' => \$print_schemes,
        'project|p=s' => \$project,
        'help|h!' => \&$handler_help,
    );


    ## end generated code

    if (not $project) {
        $project = capturex("jkd", "select-project");
    }

    my $issue_type_json;
    if (not $issue_type or not $issue_type =~ m/^\d+$/) {
        $issue_type_json = from_json(capturex("jkd", "select-issuetype", "-i", "$issue_type", "-p", "${project}", "--json-data"));
        $issue_type = $issue_type_json->{id};
    }

    my %required_fields;

    for (@field_value) {
        if (m/(.*?)=(.*)/s) {
            my ($field, $value) = ($1, $2);
            $required_fields{$field} = $value;
        } else {
            die "$_ not format of FIELD=VALUE?"
        }
    }

    if ($fields_json) {
        my $required_fields = $json->decode(scalar capturex("jkd", "customfield-json-names2ids", "-n", ("$fields_json"), "-f", decode_utf8(to_json($issue_type_json->{fields}))));

        %required_fields = %$required_fields;
        $required_fields{project} = {
            key => $project,
        };

        $required_fields{issuetype} = {
            id => $issue_type,
        };

    } else {
        work_with_all_fields($project, $issue_type, %required_fields, {"print-schemes" => ${print_schemes}});

        $required_fields{project} = {
            key => $project,
        };

        $required_fields{issuetype} = {
            id => $issue_type,
        };
    }

    say STDERR "required_fields is " . decode_utf8($json->encode(\%required_fields)) if $jkd_verbose;

    if ($assign_to_myself) {
        $required_fields{assignee} = {
            name => $ENV{scm_jira_user}
        } unless exists $required_fields{assignee};
    }

    my $ua = LWP::UserAgent->new;
    say "json is:\n", decode_utf8($json->encode({ fields => \%required_fields })) if $jkd_verbose;

    for (my $try = 0; $try < 3; $try++) {
        my $request = POST jkd_url_for_api("rest/api/2/issue"),
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            "charset" => "utf-8",
            Content => encode_json {
                fields => \%required_fields,
            };
        my $response = $ua->request($request);

        say STDERR "POST \@${try} response code:" . $response->code, "result: ", decode_utf8($response->content);


        if ($response->is_success){
            print decode_utf8($response->content);
            exit 0;
        }

        my $errors = decode_json($response->content)->{errors};
        for (keys %$errors) {
            # die "Can't find $_" unless $required_fields{$_};
            say STDERR "Delete $_ and try again";
            delete $required_fields{$_};
        }
    }
    die "Can't create issue";
}

transition of an issue

如果没有写明 transition,就让用户选择当前所有的可能的 transition

use v5.10;


add comment to an issue

sub jkd_comment(@) {
    ## start code-generator "^\\s *#\\s *"
    # generate-getopt -l -s perl i:issue c:comment @once '?"以前已经添加过的 comment,就不会再重复添加了"'
    ## end code-generator
    ## start generated code
    use Getopt::Long;

    Getopt::Long::Configure("default");

    local @ARGV = @_;

    my $comment = "";
    my $issue = "";
    my $once = 0;

    my $handler_help = sub {
        print ;
        print "\n\n选项和参数:\n";
        printf "%6s", '-c, ';
        printf "%-24s", '--comment=COMMENT';
        if (length('--comment=COMMENT') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-i, ';
        printf "%-24s", '--issue=ISSUE';
        if (length('--issue=ISSUE') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '';
        printf "%-24s", '--[no]once';
        if (length('--[no]once') > 24 and length("以前已经添加过的 comment,就不会再重复添加了") > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", "以前已经添加过的 comment,就不会再重复添加了";
        print "\n";

        exit(0);
    };

    GetOptions (
        'comment|c=s' => \$comment,
        'issue|i=s' => \$issue,
        'once!' => \$once,
        'help|h!' => \&$handler_help,
    );


    ## end generated code

    if ($once) {
        my $jira_issue = $json->decode(scalar capture("jkd rest issue/$issue"));
        for (@{$jira_issue->{fields}{comment}{comments}}) {
            if ($_->{body} eq "$comment") {
                say "$comment already exists";
                exit 0;
            }
        }
    }

    my $ua = LWP::UserAgent->new;

    my $request = PUT jkd_url_for_api("rest/api/2/issue/${issue}"),
        'Content-Type' => 'application/json',
        'Accept' => 'application/json',
        "charset" => "utf-8",
        Content => encode_json {
            update => {
                comment => [
                    {
                        add =>
                        {
                            body => $comment
                        }
                    }
                    ]
            }
    };
    my $res = $ua->request($request);

    say "PUT res code:" . $res->code, "result: ", $res->content;
    die sprintf("invalid request result: code = %d, content = '%s', for request: %s", $res->code, $res->content, decode_utf8($request->as_string)) if ($res->code < 200 or $res->code >= 300);

}

sub jkd_get_comment(@) {
    ## start code-generator "^\\s *#\\s *"
    # generate-getopt -s perl -l -P i:issue n:nth-comment=-1 c:comment '?"如果指定,在注释中找到此参数的话,即退出"'
    ## end code-generator
    ## start generated code
    use Getopt::Long;

    Getopt::Long::Configure("posix_default");

    local @ARGV = @_;

    my $comment = "";
    my $issue = "";
    my $nth_comment = -1;

    my $handler_help = sub {
        print ;
        print "\n\n选项和参数:\n";
        printf "%6s", '-c, ';
        printf "%-24s", '--comment=COMMENT';
        if (length('--comment=COMMENT') > 24 and length("如果指定,在注释中找到此参数的话,即退出") > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", "如果指定,在注释中找到此参数的话,即退出";
        print "\n";
        printf "%6s", '-i, ';
        printf "%-24s", '--issue=ISSUE';
        if (length('--issue=ISSUE') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-n, ';
        printf "%-24s", '--nth-comment=NTH-COMMENT';
        if (length('--nth-comment=NTH-COMMENT') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";

        exit(0);
    };

    GetOptions (
        'comment|c=s' => \$comment,
        'issue|i=s' => \$issue,
        'nth-comment|n=s' => \$nth_comment,
        'help|h!' => \&$handler_help,
        );


    ## end generated code
    my $jira_issue = $json->decode(scalar capture("jkd rest issue/$issue"));
    if ($comment) {
        for (@{$jira_issue->{fields}{comment}{comments}}) {
            if ($_->{body} eq "$comment") {
                exit 0;
            }
        }
        exit 1;
    }

    print $jira_issue->{fields}{comment}{comments}[$nth_comment]{body};
}

move issue to a sprint

use HTTP::Request::Common;
use LWP::UserAgent;
use JSON;


sub jkd_mits(@) {
    ## start code-generator "^\\s *#\\s *"
    # generate-getopt -s perl -l i:issue s:sprint b:board
    ## end code-generator
    ## start generated code
    use Getopt::Long;

    Getopt::Long::Configure("default");

    local @ARGV = @_;

    my $board = "";
    my $issue = "";
    my $sprint = "";

    my $handler_help = sub {
        print ;
        print "\n\n选项和参数:\n";
        printf "%6s", '-b, ';
        printf "%-24s", '--board=BOARD';
        if (length('--board=BOARD') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-i, ';
        printf "%-24s", '--issue=ISSUE';
        if (length('--issue=ISSUE') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";
        printf "%6s", '-s, ';
        printf "%-24s", '--sprint=SPRINT';
        if (length('--sprint=SPRINT') > 24 and length() > 0) {
            print "\n";
            printf "%30s", "";
        }
        printf "%s", ;
        print "\n";

        exit(0);
    };

    GetOptions (
        'board|b=s' => \$board,
        'issue|i=s' => \$issue,
        'sprint|s=s' => \$sprint,
        'help|h!' => \&$handler_help,
        );


    ## end generated code

    use v5.10;

    if (not $sprint) {
        if (not $board or $board !~ m/^\d+$/) {
            my $json_boards = decode_json get("rest/agile/1.0/board/")->content;
            $board = select_args("-i", $board, "-p", "which board do you want? (should be scrum, not kanban)", sort {$a cmp $b} map {sprintf "%s: %s", $_->{id}, $_->{name}} @{$json_boards->{values}});
            $board =~ s,:.*,,;
        }
        if ($board) {
            my $json_sprints = decode_json get("rest/agile/1.0/board/$board/sprint")->content;
            $sprint = $json_sprints->{values}[-1]{id};
        } else {
            die "Must specify one of sprint or board, when using board, the last sprint will be used";
        }
    }

    my $ua = LWP::UserAgent->new;

    my $request = POST jkd_url_for_api("/rest/agile/1.0/sprint/${sprint}/issue"),
        'Content-Type' => 'application/json',
        'Accept' => 'application/json',
        "charset" => "utf-8",
        Content => encode_json {
            issues => [
                "$issue"
                ]
    };
    my $response = $ua->request($request);

    say "POST response code:" . $response->code, "result: ", decode_utf8($response->content);


}