| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | # ========================================================================== # | 
| 2 |  |  |  |  |  |  | # lib/Git/Hooks/Youtrack.pm - Github Hooks for youtrack | 
| 3 |  |  |  |  |  |  | # ========================================================================== # | 
| 4 |  |  |  |  |  |  |  | 
| 5 |  |  |  |  |  |  | package Git::Hooks::CheckYoutrack; | 
| 6 |  |  |  |  |  |  |  | 
| 7 | 1 |  |  | 1 |  | 626 | use strict; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 27 |  | 
| 8 | 1 |  |  | 1 |  | 4 | use warnings; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 34 |  | 
| 9 | 1 |  |  | 1 |  | 6 | use utf8; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 10 |  | 
| 10 | 1 |  |  | 1 |  | 29 | use Log::Any '$log'; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 12 |  | 
| 11 | 1 |  |  | 1 |  | 282 | use Path::Tiny; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 37 |  | 
| 12 | 1 |  |  | 1 |  | 582 | use Git::Hooks; | 
|  | 1 |  |  |  |  | 4940 |  | 
|  | 1 |  |  |  |  | 99 |  | 
| 13 | 1 |  |  | 1 |  | 583 | use LWP::UserAgent; | 
|  | 1 |  |  |  |  | 117672 |  | 
|  | 1 |  |  |  |  | 446 |  | 
| 14 | 1 |  |  | 1 |  | 14291 | use URI::Builder; | 
|  | 1 |  |  |  |  | 2742 |  | 
|  | 1 |  |  |  |  | 35 |  | 
| 15 | 1 |  |  | 1 |  | 677 | use JSON::XS; | 
|  | 1 |  |  |  |  | 3822 |  | 
|  | 1 |  |  |  |  | 2495 |  | 
| 16 |  |  |  |  |  |  |  | 
| 17 |  |  |  |  |  |  | $Git::Hooks::CheckYoutrack::VERSION = '1.0.2'; | 
| 18 |  |  |  |  |  |  |  | 
| 19 |  |  |  |  |  |  | =head1 NAME | 
| 20 |  |  |  |  |  |  |  | 
| 21 |  |  |  |  |  |  | Git::Hooks::CheckYoutrack - Git::Hooks plugin which requires youtrack ticket number on each commit message | 
| 22 |  |  |  |  |  |  |  | 
| 23 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 24 |  |  |  |  |  |  |  | 
| 25 |  |  |  |  |  |  | As a C plugin you don't use this Perl module directly. Instead, you | 
| 26 |  |  |  |  |  |  | may configure it in a Git configuration file like this: | 
| 27 |  |  |  |  |  |  |  | 
| 28 |  |  |  |  |  |  | [githooks] | 
| 29 |  |  |  |  |  |  |  | 
| 30 |  |  |  |  |  |  | # Enable the plugin | 
| 31 |  |  |  |  |  |  | plugin = CheckYoutrack | 
| 32 |  |  |  |  |  |  |  | 
| 33 |  |  |  |  |  |  | [githooks "checkyoutrack"] | 
| 34 |  |  |  |  |  |  |  | 
| 35 |  |  |  |  |  |  | # '/youtrack' will be appended to this host | 
| 36 |  |  |  |  |  |  | youtrack-host = "https://example.myjetbrains.com" | 
| 37 |  |  |  |  |  |  |  | 
| 38 |  |  |  |  |  |  | # Refer: https://www.jetbrains.com/help/youtrack/standalone/Manage-Permanent-Token.html | 
| 39 |  |  |  |  |  |  | # to create a Bearer token | 
| 40 |  |  |  |  |  |  | # You can also set YoutrackToken ENV instead of this config | 
| 41 |  |  |  |  |  |  | youtrack-token = "" | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | # Regular expression to match for Youtrack ticket id | 
| 44 |  |  |  |  |  |  | matchkey = "^((?:P)(?:AY|\\d+)-\\d+)" | 
| 45 |  |  |  |  |  |  |  | 
| 46 |  |  |  |  |  |  | # Setting this flag will aborts the commit if valid Youtrack number not found | 
| 47 |  |  |  |  |  |  | # Shows a warning message otherwise - default false | 
| 48 |  |  |  |  |  |  | required = true | 
| 49 |  |  |  |  |  |  |  | 
| 50 |  |  |  |  |  |  | # Print the fetched youtrack ticket details like Assignee, State etc.., | 
| 51 |  |  |  |  |  |  | # default false | 
| 52 |  |  |  |  |  |  | print-info = true | 
| 53 |  |  |  |  |  |  |  | 
| 54 |  |  |  |  |  |  |  | 
| 55 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 56 |  |  |  |  |  |  |  | 
| 57 |  |  |  |  |  |  | This plugin hooks the following git hooks to guarantee that every commit message | 
| 58 |  |  |  |  |  |  | cites a valid Youtrack Id in the log message, so that you can be certain that | 
| 59 |  |  |  |  |  |  | every commit message has a valid link to the Youtrack ticket. Refer L | 
| 60 |  |  |  |  |  |  | for steps to install and use Git::Hooks | 
| 61 |  |  |  |  |  |  |  | 
| 62 |  |  |  |  |  |  | This plugin also hooks prepare-commit-msg to pre-populate youtrack ticket sumary on the | 
| 63 |  |  |  |  |  |  | commit message if the current working branch name is starting with the valid ticket number | 
| 64 |  |  |  |  |  |  |  | 
| 65 |  |  |  |  |  |  | =head1 METHODS | 
| 66 |  |  |  |  |  |  |  | 
| 67 |  |  |  |  |  |  | =cut | 
| 68 |  |  |  |  |  |  |  | 
| 69 |  |  |  |  |  |  | my $PKG = __PACKAGE__; | 
| 70 |  |  |  |  |  |  | (my $CFG = __PACKAGE__) =~ s/.*::/githooks./; | 
| 71 |  |  |  |  |  |  |  | 
| 72 |  |  |  |  |  |  | # =========================================================================== # | 
| 73 |  |  |  |  |  |  |  | 
| 74 |  |  |  |  |  |  | =head2 B, B | 
| 75 |  |  |  |  |  |  |  | 
| 76 |  |  |  |  |  |  | These hooks are invoked during the commit, to check if the commit message | 
| 77 |  |  |  |  |  |  | starts with a valid Youtrack ticket Id. | 
| 78 |  |  |  |  |  |  |  | 
| 79 |  |  |  |  |  |  | =cut | 
| 80 |  |  |  |  |  |  |  | 
| 81 |  |  |  |  |  |  | sub check_commit_msg { | 
| 82 | 0 |  |  | 0 | 0 |  | my ($git, $message, $commit_id) = @_; | 
| 83 |  |  |  |  |  |  |  | 
| 84 |  |  |  |  |  |  | # Skip for empty message | 
| 85 | 0 | 0 | 0 |  |  |  | return 'no_check' if (!$message || $message =~ /^[\n\r]$/g); | 
| 86 |  |  |  |  |  |  |  | 
| 87 | 0 |  |  |  |  |  | $log->info("Checking commit message: $message"); | 
| 88 |  |  |  |  |  |  |  | 
| 89 | 0 |  |  |  |  |  | my $yt_id = _get_youtrack_id($git, $message); | 
| 90 |  |  |  |  |  |  |  | 
| 91 | 0 | 0 |  |  |  |  | if (!$yt_id) { | 
| 92 | 0 |  |  |  |  |  | return _show_error($git, "Missing youtrack ticket id in your message: $message"); | 
| 93 |  |  |  |  |  |  | } | 
| 94 |  |  |  |  |  |  |  | 
| 95 | 0 |  |  |  |  |  | $log->info("Extracted Youtrack ticket id from message as: $yt_id"); | 
| 96 |  |  |  |  |  |  |  | 
| 97 | 0 |  |  |  |  |  | my $yt_ticket = _get_ticket($git, $yt_id); | 
| 98 |  |  |  |  |  |  |  | 
| 99 | 0 | 0 |  |  |  |  | if (!$yt_ticket) { | 
| 100 | 0 |  |  |  |  |  | return _show_error($git, "Youtrack ticket not found with ID: $yt_id"); | 
| 101 |  |  |  |  |  |  | } | 
| 102 |  |  |  |  |  |  |  | 
| 103 | 0 | 0 | 0 |  |  |  | if ($yt_ticket && $git->get_config_boolean($CFG => 'print-info')) { | 
| 104 | 0 |  |  |  |  |  | print '-' x 80 . "\n"; | 
| 105 | 0 | 0 |  |  |  |  | print "For git commit:  $commit_id\n" if($commit_id); | 
| 106 | 0 |  |  |  |  |  | print "Youtrack ticket: $yt_ticket->{ticket_id}\n"; | 
| 107 | 0 |  |  |  |  |  | print "Summary:         $yt_ticket->{summary}\n"; | 
| 108 | 0 |  |  |  |  |  | print "Current status:  $yt_ticket->{State}\n"; | 
| 109 | 0 |  |  |  |  |  | print "Assigned to:     $yt_ticket->{Assignee}\n"; | 
| 110 | 0 |  |  |  |  |  | print "Ticket Link:     $yt_ticket->{WebLink}\n"; | 
| 111 | 0 |  |  |  |  |  | print '-' x 80 . "\n"; | 
| 112 |  |  |  |  |  |  | } | 
| 113 |  |  |  |  |  |  |  | 
| 114 | 0 |  |  |  |  |  | return 0; | 
| 115 |  |  |  |  |  |  | } | 
| 116 |  |  |  |  |  |  |  | 
| 117 |  |  |  |  |  |  | # =========================================================================== # | 
| 118 |  |  |  |  |  |  |  | 
| 119 |  |  |  |  |  |  | sub check_message_file { | 
| 120 | 0 |  |  | 0 | 0 |  | my ($git, $commit_msg_file) = @_; | 
| 121 |  |  |  |  |  |  |  | 
| 122 | 0 |  |  |  |  |  | $log->debug(__PACKAGE__ . "::check_message_file($commit_msg_file)"); | 
| 123 |  |  |  |  |  |  |  | 
| 124 | 0 |  |  |  |  |  | _setup_config($git); | 
| 125 |  |  |  |  |  |  |  | 
| 126 | 0 |  |  |  |  |  | my $msg = _get_message_from_file($git, $commit_msg_file); | 
| 127 |  |  |  |  |  |  |  | 
| 128 |  |  |  |  |  |  | # Remove comment lines from the message file contents. | 
| 129 | 0 |  |  |  |  |  | $msg =~ s/^#[^\n]*\n//mgs; | 
| 130 |  |  |  |  |  |  |  | 
| 131 | 0 |  |  |  |  |  | return check_commit_msg($git, $msg); | 
| 132 |  |  |  |  |  |  | } | 
| 133 |  |  |  |  |  |  |  | 
| 134 |  |  |  |  |  |  | # =========================================================================== # | 
| 135 |  |  |  |  |  |  |  | 
| 136 |  |  |  |  |  |  | sub _show_error { | 
| 137 | 0 |  |  | 0 |  |  | my ($git, $msg) = @_; | 
| 138 | 0 |  |  |  |  |  | $log->error($msg); | 
| 139 | 0 | 0 |  |  |  |  | if ($git->get_config_boolean($CFG => 'required')) { | 
| 140 | 0 |  |  |  |  |  | $git->fault("ERROR: $msg"); | 
| 141 | 0 |  |  |  |  |  | return 1; | 
| 142 |  |  |  |  |  |  | } | 
| 143 |  |  |  |  |  |  | else { | 
| 144 | 0 |  |  |  |  |  | print "WARNING: $msg\n"; | 
| 145 | 0 |  |  |  |  |  | return 0; | 
| 146 |  |  |  |  |  |  | } | 
| 147 |  |  |  |  |  |  | } | 
| 148 |  |  |  |  |  |  |  | 
| 149 |  |  |  |  |  |  | # =========================================================================== # | 
| 150 |  |  |  |  |  |  |  | 
| 151 |  |  |  |  |  |  | =head2 B | 
| 152 |  |  |  |  |  |  |  | 
| 153 |  |  |  |  |  |  | This hook is for remote repository and should be installed and configured at the remote git server. | 
| 154 |  |  |  |  |  |  | Checks for youtrack ticket on each commit message pushed to the remote repository and deny push | 
| 155 |  |  |  |  |  |  | if its not found and its required = true in the config, shows a warning message on client side | 
| 156 |  |  |  |  |  |  | if config required = false but accepts the push. | 
| 157 |  |  |  |  |  |  |  | 
| 158 |  |  |  |  |  |  | =cut | 
| 159 |  |  |  |  |  |  |  | 
| 160 |  |  |  |  |  |  | sub check_affected_refs { | 
| 161 | 0 |  |  | 0 | 0 |  | my ($git) = @_; | 
| 162 |  |  |  |  |  |  |  | 
| 163 | 0 |  |  |  |  |  | $log->debug(__PACKAGE__ . "::check_affected_refs"); | 
| 164 |  |  |  |  |  |  |  | 
| 165 | 0 |  |  |  |  |  | _setup_config($git); | 
| 166 |  |  |  |  |  |  |  | 
| 167 | 0 |  |  |  |  |  | my $errors = 0; | 
| 168 |  |  |  |  |  |  |  | 
| 169 | 0 |  |  |  |  |  | foreach my $ref ($git->get_affected_refs()) { | 
| 170 | 0 | 0 |  |  |  |  | next unless $git->is_reference_enabled($ref); | 
| 171 | 0 | 0 |  |  |  |  | if(check_ref($git, $ref)) { | 
| 172 | 0 |  |  |  |  |  | ++$errors; | 
| 173 |  |  |  |  |  |  | } | 
| 174 |  |  |  |  |  |  | } | 
| 175 |  |  |  |  |  |  |  | 
| 176 | 0 | 0 |  |  |  |  | if($errors) { | 
| 177 | 0 |  |  |  |  |  | return _show_error($git, "Some of your commit message missing a valid youtrack ticket"); | 
| 178 |  |  |  |  |  |  | } | 
| 179 |  |  |  |  |  |  |  | 
| 180 | 0 |  |  |  |  |  | return 0; | 
| 181 |  |  |  |  |  |  | } | 
| 182 |  |  |  |  |  |  |  | 
| 183 |  |  |  |  |  |  | # =========================================================================== # | 
| 184 |  |  |  |  |  |  |  | 
| 185 |  |  |  |  |  |  | sub check_ref { | 
| 186 | 0 |  |  | 0 | 0 |  | my ($git, $ref) = @_; | 
| 187 |  |  |  |  |  |  |  | 
| 188 | 0 |  |  |  |  |  | my $errors = 0; | 
| 189 |  |  |  |  |  |  |  | 
| 190 | 0 |  |  |  |  |  | foreach my $commit ($git->get_affected_ref_commits($ref)) { | 
| 191 | 0 |  |  |  |  |  | local $log->context->{commit_id} = $commit->commit; | 
| 192 | 0 |  |  |  |  |  | $log->info("Commit from : ", $commit->author_name, " Git Ref: ", $ref); | 
| 193 | 0 | 0 |  |  |  |  | if(check_commit_msg($git, $commit->message, $commit->commit)) { | 
| 194 | 0 |  |  |  |  |  | ++$errors; | 
| 195 |  |  |  |  |  |  | } | 
| 196 |  |  |  |  |  |  | } | 
| 197 |  |  |  |  |  |  |  | 
| 198 | 0 |  |  |  |  |  | return $errors; | 
| 199 |  |  |  |  |  |  | } | 
| 200 |  |  |  |  |  |  |  | 
| 201 |  |  |  |  |  |  | # =========================================================================== # | 
| 202 |  |  |  |  |  |  |  | 
| 203 |  |  |  |  |  |  | =head2 B | 
| 204 |  |  |  |  |  |  |  | 
| 205 |  |  |  |  |  |  | This hook is invoked before a commit, to check if the current branch name start with | 
| 206 |  |  |  |  |  |  | a valid youtrack ticket id and pre-populates the commit message with youtrack ticket: summary | 
| 207 |  |  |  |  |  |  |  | 
| 208 |  |  |  |  |  |  | =cut | 
| 209 |  |  |  |  |  |  |  | 
| 210 |  |  |  |  |  |  | sub add_youtrack_summary { | 
| 211 | 0 |  |  | 0 | 0 |  | my ($git, $commit_msg_file) = @_; | 
| 212 |  |  |  |  |  |  |  | 
| 213 | 0 |  |  |  |  |  | $log->debug(__PACKAGE__ . "::add_youtrack_summary($commit_msg_file)"); | 
| 214 |  |  |  |  |  |  |  | 
| 215 | 0 |  |  |  |  |  | _setup_config($git); | 
| 216 |  |  |  |  |  |  |  | 
| 217 | 0 |  |  |  |  |  | my $msg      = _get_message_from_file($git, $commit_msg_file); | 
| 218 | 0 |  |  |  |  |  | my $msg_copy = $msg; | 
| 219 |  |  |  |  |  |  |  | 
| 220 |  |  |  |  |  |  | # Remove comment lines and empty lines from the message file contents. | 
| 221 | 0 |  |  |  |  |  | $msg =~ s/^#[^\n]*\n//mgs; | 
| 222 | 0 |  |  |  |  |  | $msg =~ s/^\n*\n$//msg; | 
| 223 |  |  |  |  |  |  |  | 
| 224 |  |  |  |  |  |  | # Don't do anything if message already exist (user used -m option) | 
| 225 | 0 | 0 |  |  |  |  | if ($msg) { | 
| 226 | 0 |  |  |  |  |  | $log->info("Message exist: $msg"); | 
| 227 | 0 |  |  |  |  |  | return 0; | 
| 228 |  |  |  |  |  |  | } | 
| 229 |  |  |  |  |  |  |  | 
| 230 | 0 |  |  |  |  |  | my $current_branch = $git->get_current_branch(); | 
| 231 |  |  |  |  |  |  |  | 
| 232 |  |  |  |  |  |  | # Extract current branch name | 
| 233 | 0 |  |  |  |  |  | $current_branch =~ s/.+\/(.+)/$1/; | 
| 234 |  |  |  |  |  |  |  | 
| 235 | 0 |  |  |  |  |  | my $yt_id = _get_youtrack_id($git, $current_branch); | 
| 236 |  |  |  |  |  |  |  | 
| 237 | 0 | 0 |  |  |  |  | if (!$yt_id) { | 
| 238 | 0 |  |  |  |  |  | $log->warn("No youtrack id in your working branch"); | 
| 239 | 0 |  |  |  |  |  | return 0; | 
| 240 |  |  |  |  |  |  | } | 
| 241 |  |  |  |  |  |  |  | 
| 242 | 0 |  |  |  |  |  | my $task = _get_ticket($git, $yt_id); | 
| 243 |  |  |  |  |  |  |  | 
| 244 | 0 | 0 |  |  |  |  | if (!$task) { | 
| 245 | 0 |  |  |  |  |  | $log->warn("Your branch name does not match with youtrack ticket"); | 
| 246 | 0 |  |  |  |  |  | return 0; | 
| 247 |  |  |  |  |  |  | } | 
| 248 |  |  |  |  |  |  |  | 
| 249 | 0 |  |  |  |  |  | my $ticket_msg = "$yt_id: $task->{summary}\n"; | 
| 250 |  |  |  |  |  |  |  | 
| 251 | 0 |  |  |  |  |  | $log->info("Pre-populating commit message as: $ticket_msg"); | 
| 252 |  |  |  |  |  |  |  | 
| 253 | 0 | 0 |  |  |  |  | open my $out, '>', path($commit_msg_file) or die "Can't write new file: $!"; | 
| 254 | 0 |  |  |  |  |  | print $out $ticket_msg; | 
| 255 | 0 |  |  |  |  |  | print $out $msg_copy; | 
| 256 | 0 |  |  |  |  |  | close $out; | 
| 257 |  |  |  |  |  |  | } | 
| 258 |  |  |  |  |  |  |  | 
| 259 |  |  |  |  |  |  | # =========================================================================== # | 
| 260 |  |  |  |  |  |  |  | 
| 261 |  |  |  |  |  |  | sub _get_message_from_file { | 
| 262 | 0 |  |  | 0 |  |  | my ($git, $file) = @_; | 
| 263 |  |  |  |  |  |  |  | 
| 264 | 0 |  |  |  |  |  | my $msg = eval { path($file)->slurp }; | 
|  | 0 |  |  |  |  |  |  | 
| 265 | 0 | 0 | 0 |  |  |  | defined $msg | 
| 266 |  |  |  |  |  |  | or $git->fault("Cannot open file '$file' for reading:", {details => $@}) | 
| 267 |  |  |  |  |  |  | and return 0; | 
| 268 |  |  |  |  |  |  |  | 
| 269 | 0 |  |  |  |  |  | return $msg; | 
| 270 |  |  |  |  |  |  | } | 
| 271 |  |  |  |  |  |  |  | 
| 272 |  |  |  |  |  |  | # =========================================================================== # | 
| 273 |  |  |  |  |  |  |  | 
| 274 |  |  |  |  |  |  | # Setup default configs | 
| 275 |  |  |  |  |  |  | sub _setup_config { | 
| 276 | 0 |  |  | 0 |  |  | my ($git) = @_; | 
| 277 |  |  |  |  |  |  |  | 
| 278 | 0 |  |  |  |  |  | my $config = $git->get_config(); | 
| 279 |  |  |  |  |  |  |  | 
| 280 | 0 |  | 0 |  |  |  | $config->{lc $CFG} //= {}; | 
| 281 |  |  |  |  |  |  |  | 
| 282 | 0 |  |  |  |  |  | my $default = $config->{lc $CFG}; | 
| 283 |  |  |  |  |  |  |  | 
| 284 |  |  |  |  |  |  | # Default matchkey for matching Youtrack ids (P3-1234 || PAY-1234) keys. | 
| 285 | 0 |  | 0 |  |  |  | $default->{matchkey} //= ['^(P(?:AY|\d+)-\d+)']; | 
| 286 |  |  |  |  |  |  |  | 
| 287 | 0 |  | 0 |  |  |  | $default->{required} //= ['false']; | 
| 288 |  |  |  |  |  |  |  | 
| 289 | 0 |  | 0 |  |  |  | $default->{'print-info'} //= ['false']; | 
| 290 |  |  |  |  |  |  |  | 
| 291 | 0 |  |  |  |  |  | return; | 
| 292 |  |  |  |  |  |  | } | 
| 293 |  |  |  |  |  |  |  | 
| 294 |  |  |  |  |  |  | # =========================================================================== # | 
| 295 |  |  |  |  |  |  |  | 
| 296 |  |  |  |  |  |  | # Tries to get a valid youtrack ticket and return a HashRef with ticket details if found success | 
| 297 |  |  |  |  |  |  | sub _get_ticket { | 
| 298 | 0 |  |  | 0 |  |  | my ($git, $ticket_id) = @_; | 
| 299 |  |  |  |  |  |  |  | 
| 300 | 0 |  |  |  |  |  | my $yt_token = $git->get_config($CFG => 'youtrack-token'); | 
| 301 |  |  |  |  |  |  |  | 
| 302 | 0 | 0 |  |  |  |  | $yt_token = $ENV{YoutrackToken} if (!$yt_token); | 
| 303 |  |  |  |  |  |  |  | 
| 304 | 0 | 0 |  |  |  |  | if (!$yt_token) { | 
| 305 | 0 |  |  |  |  |  | my $error = "Please set Youtrack permanent token in ENV YoutrackToken\n"; | 
| 306 | 0 |  |  |  |  |  | $error .= "Refer: https://www.jetbrains.com/help/youtrack/standalone/Manage-Permanent-Token.html\n"; | 
| 307 | 0 |  |  |  |  |  | $error .= "to generate a token\n"; | 
| 308 | 0 |  |  |  |  |  | $git->fault($error); | 
| 309 | 0 |  |  |  |  |  | return; | 
| 310 |  |  |  |  |  |  | } | 
| 311 |  |  |  |  |  |  |  | 
| 312 | 0 |  |  |  |  |  | my $yt_host = $git->get_config($CFG => 'youtrack-host'); | 
| 313 | 0 |  |  |  |  |  | my $yt      = URI::Builder->new(uri => $yt_host); | 
| 314 |  |  |  |  |  |  |  | 
| 315 | 0 |  | 0 |  |  |  | my $ua = $git->{yt_ua} //= LWP::UserAgent->new(); | 
| 316 | 0 |  |  |  |  |  | $ua->default_header('Authorization' => "Bearer $yt_token"); | 
| 317 | 0 |  |  |  |  |  | $ua->default_header('Accept'        => 'application/json'); | 
| 318 | 0 |  |  |  |  |  | $ua->default_header('Content-Type'  => 'application/json'); | 
| 319 |  |  |  |  |  |  |  | 
| 320 | 0 |  |  |  |  |  | my $ticket_fields = 'fields=numberInProject,project(shortName),summary,customFields(name,value(name))'; | 
| 321 |  |  |  |  |  |  |  | 
| 322 | 0 |  |  |  |  |  | $yt->path_segments("youtrack", "api", "issues", $ticket_id); | 
| 323 |  |  |  |  |  |  |  | 
| 324 | 0 |  |  |  |  |  | my $url = $yt->as_string . "?$ticket_fields"; | 
| 325 |  |  |  |  |  |  |  | 
| 326 | 0 |  |  |  |  |  | my $ticket = $ua->get($url); | 
| 327 |  |  |  |  |  |  |  | 
| 328 | 0 | 0 |  |  |  |  | if (!$ticket->is_success) { | 
| 329 | 0 |  |  |  |  |  | $log->error("Youtrack fetch failed with status: ", $ticket->status_line); | 
| 330 | 0 |  |  |  |  |  | return; | 
| 331 |  |  |  |  |  |  | } | 
| 332 |  |  |  |  |  |  |  | 
| 333 | 0 |  |  |  |  |  | my $json           = decode_json($ticket->decoded_content); | 
| 334 | 0 |  |  |  |  |  | my $ticket_details = _process_ticket($json); | 
| 335 |  |  |  |  |  |  |  | 
| 336 | 0 | 0 |  |  |  |  | if (!$ticket_details->{ticket_id}) { | 
| 337 | 0 |  |  |  |  |  | $log->error("No valid youtrack ticket found"); | 
| 338 | 0 |  |  |  |  |  | return; | 
| 339 |  |  |  |  |  |  | } | 
| 340 |  |  |  |  |  |  |  | 
| 341 | 0 | 0 |  |  |  |  | $ticket_details->{Assignee} = 'Unassigned' if (!$ticket_details->{Assignee}); | 
| 342 |  |  |  |  |  |  |  | 
| 343 | 0 |  |  |  |  |  | $yt->path_segments('youtrack', 'issue', $ticket_id); | 
| 344 | 0 |  |  |  |  |  | $ticket_details->{WebLink} = $yt->as_string; | 
| 345 |  |  |  |  |  |  |  | 
| 346 | 0 |  |  |  |  |  | return $ticket_details; | 
| 347 |  |  |  |  |  |  | } | 
| 348 |  |  |  |  |  |  |  | 
| 349 |  |  |  |  |  |  | # =========================================================================== # | 
| 350 |  |  |  |  |  |  |  | 
| 351 |  |  |  |  |  |  | # Helper method to process the response from Youtrack API | 
| 352 |  |  |  |  |  |  | sub _process_ticket { | 
| 353 | 0 |  |  | 0 |  |  | my $json = shift; | 
| 354 |  |  |  |  |  |  |  | 
| 355 | 0 | 0 |  |  |  |  | return if (!$json); | 
| 356 | 0 |  |  |  |  |  | my $ticket; | 
| 357 |  |  |  |  |  |  |  | 
| 358 | 0 |  |  |  |  |  | $ticket->{summary}   = $json->{summary}; | 
| 359 | 0 |  |  |  |  |  | $ticket->{type}      = $json->{'$' . 'type'}; | 
| 360 | 0 |  |  |  |  |  | $ticket->{ticket_id} = $json->{numberInProject}; | 
| 361 |  |  |  |  |  |  |  | 
| 362 | 0 | 0 | 0 |  |  |  | if ($json->{project} && $json->{project}->{shortName}) { | 
| 363 | 0 |  |  |  |  |  | $ticket->{ticket_id} = $json->{project}->{shortName} . '-' . $ticket->{ticket_id}; | 
| 364 |  |  |  |  |  |  | } | 
| 365 |  |  |  |  |  |  |  | 
| 366 | 0 | 0 |  |  |  |  | if ($json->{customFields}) { | 
| 367 | 0 |  |  |  |  |  | foreach my $field (@{$json->{customFields}}) { | 
|  | 0 |  |  |  |  |  |  | 
| 368 |  |  |  |  |  |  |  | 
| 369 | 0 | 0 |  |  |  |  | if (ref $field->{value} eq 'HASH') { | 
|  |  | 0 |  |  |  |  |  | 
| 370 | 0 |  |  |  |  |  | $ticket->{$field->{name}} = $field->{value}->{name}; | 
| 371 |  |  |  |  |  |  | } | 
| 372 |  |  |  |  |  |  | elsif (ref $field->{value} eq 'ARRAY') { | 
| 373 | 0 |  |  |  |  |  | foreach my $val (@{$field->{value}}) { | 
|  | 0 |  |  |  |  |  |  | 
| 374 | 0 |  |  |  |  |  | $ticket->{$field->{name}} = join(',', $val->{name}); | 
| 375 |  |  |  |  |  |  | } | 
| 376 |  |  |  |  |  |  | } | 
| 377 |  |  |  |  |  |  | else { | 
| 378 | 0 |  |  |  |  |  | $ticket->{$field->{name}} = $field->{value}; | 
| 379 |  |  |  |  |  |  | } | 
| 380 |  |  |  |  |  |  | } | 
| 381 |  |  |  |  |  |  | } | 
| 382 |  |  |  |  |  |  |  | 
| 383 | 0 |  |  |  |  |  | return $ticket; | 
| 384 |  |  |  |  |  |  | } | 
| 385 |  |  |  |  |  |  |  | 
| 386 |  |  |  |  |  |  | # =========================================================================== # | 
| 387 |  |  |  |  |  |  |  | 
| 388 |  |  |  |  |  |  | # Check and return a youtrack Id in the given string based on the matchkey regex | 
| 389 |  |  |  |  |  |  | sub _get_youtrack_id { | 
| 390 | 0 |  |  | 0 |  |  | my ($git, $message) = @_; | 
| 391 |  |  |  |  |  |  |  | 
| 392 | 0 |  |  |  |  |  | my $matchkey = $git->get_config($CFG => 'matchkey'); | 
| 393 |  |  |  |  |  |  |  | 
| 394 | 0 |  |  |  |  |  | chomp $message; | 
| 395 |  |  |  |  |  |  |  | 
| 396 | 0 | 0 |  |  |  |  | if ($message =~ /$matchkey/i) { | 
| 397 | 0 |  |  |  |  |  | return uc($1); | 
| 398 |  |  |  |  |  |  | } | 
| 399 |  |  |  |  |  |  |  | 
| 400 | 0 |  |  |  |  |  | $log->info("\"$message\" does not match /$matchkey/"); | 
| 401 |  |  |  |  |  |  |  | 
| 402 | 0 |  |  |  |  |  | return; | 
| 403 |  |  |  |  |  |  | } | 
| 404 |  |  |  |  |  |  |  | 
| 405 |  |  |  |  |  |  | # =========================================================================== # | 
| 406 |  |  |  |  |  |  |  | 
| 407 |  |  |  |  |  |  | =head1 USAGE INSTRUCTION | 
| 408 |  |  |  |  |  |  |  | 
| 409 |  |  |  |  |  |  | Create a generic script that will be invoked by Git for every hook. Go to hooks directory of your repository, | 
| 410 |  |  |  |  |  |  | for local repository it is .git/hooks/ and for remote server it is ./hooks/ and create a simple executable perl script | 
| 411 |  |  |  |  |  |  |  | 
| 412 |  |  |  |  |  |  | $ cd /path/to/repo/.git/hooks | 
| 413 |  |  |  |  |  |  |  | 
| 414 |  |  |  |  |  |  | $ cat >git-hooks.pl <<'EOT' | 
| 415 |  |  |  |  |  |  | #!/usr/bin/env perl | 
| 416 |  |  |  |  |  |  | use Git::Hooks; | 
| 417 |  |  |  |  |  |  | run_hook($0, @ARGV); | 
| 418 |  |  |  |  |  |  | EOT | 
| 419 |  |  |  |  |  |  |  | 
| 420 |  |  |  |  |  |  | $ chmod +x git-hooks.pl | 
| 421 |  |  |  |  |  |  |  | 
| 422 |  |  |  |  |  |  | Now you should create symbolic links pointing to this perl script for each hook you are interested in | 
| 423 |  |  |  |  |  |  |  | 
| 424 |  |  |  |  |  |  | For local repository | 
| 425 |  |  |  |  |  |  |  | 
| 426 |  |  |  |  |  |  | $ cd /path/to/repo/.git/hooks | 
| 427 |  |  |  |  |  |  |  | 
| 428 |  |  |  |  |  |  | $ ln -s git-hooks.pl commit-msg | 
| 429 |  |  |  |  |  |  | $ ln -s git-hooks.pl applypatch-msg | 
| 430 |  |  |  |  |  |  | $ ln -s git-hooks.pl prepare-commit-msg | 
| 431 |  |  |  |  |  |  |  | 
| 432 |  |  |  |  |  |  | For remote repository | 
| 433 |  |  |  |  |  |  |  | 
| 434 |  |  |  |  |  |  | $ cd /path/to/repo/hooks | 
| 435 |  |  |  |  |  |  |  | 
| 436 |  |  |  |  |  |  | $ ln -s git-hooks.pl update | 
| 437 |  |  |  |  |  |  |  | 
| 438 |  |  |  |  |  |  | =cut | 
| 439 |  |  |  |  |  |  |  | 
| 440 |  |  |  |  |  |  | # Install hooks via Git::Hooks | 
| 441 |  |  |  |  |  |  | APPLYPATCH_MSG \&check_message_file; | 
| 442 |  |  |  |  |  |  | COMMIT_MSG \&check_message_file; | 
| 443 |  |  |  |  |  |  | PREPARE_COMMIT_MSG \&add_youtrack_summary; | 
| 444 |  |  |  |  |  |  | UPDATE \&check_affected_refs; | 
| 445 |  |  |  |  |  |  |  | 
| 446 |  |  |  |  |  |  | # =========================================================================== # | 
| 447 |  |  |  |  |  |  |  | 
| 448 |  |  |  |  |  |  | 1; | 
| 449 |  |  |  |  |  |  |  | 
| 450 |  |  |  |  |  |  | __END__ |