View | Details | Raw Unified | Return to bug 6473
Collapse All | Expand All

(-)a/bin/git-bz (+1 lines)
Lines 16-21 Link Here
16
# along with git-bz; if not, see <https://www.gnu.org/licenses>.
16
# along with git-bz; if not, see <https://www.gnu.org/licenses>.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
use FindBin;
20
use FindBin;
20
use lib "$FindBin::RealBin/../lib";
21
use lib "$FindBin::RealBin/../lib";
21
use GitBz::Commands;
22
use GitBz::Commands;
(-)a/lib/GitBz/Bug.pm (+70 lines)
Line 0 Link Here
1
package GitBz::Bug;
2
3
# This file is part of git-bz.
4
#
5
# git-bz is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# git-bz is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with git-bz; if not, see <https://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use GitBz::Exception;
21
22
sub get {
23
    my ( $class, $client, $number ) = @_;
24
25
    my $bug_data = $client->get_bug($number);
26
    GitBz::Exception->throw("Bug $number not found") unless $bug_data;
27
28
    return bless {
29
        client       => $client,
30
        data         => $bug_data,
31
        _attachments => undef,
32
    }, $class;
33
}
34
35
sub id                  { $_[0]->{data}->{id} }
36
sub summary             { $_[0]->{data}->{summary} }
37
sub status              { $_[0]->{data}->{status} }
38
sub resolution          { $_[0]->{data}->{resolution} }
39
sub depends_on          { $_[0]->{data}->{depends_on} || [] }
40
sub cf_patch_complexity { $_[0]->{data}->{cf_patch_complexity} }
41
42
sub attachments {
43
    my ($self) = @_;
44
45
    unless ( $self->{_attachments} ) {
46
        $self->{_attachments} = $self->{client}->get_attachments( $self->id );
47
    }
48
49
    return $self->{_attachments};
50
}
51
52
sub update {
53
    my ( $self, %params ) = @_;
54
55
    return $self->{client}->update_bug( $self->id, %params );
56
}
57
58
sub add_attachment {
59
    my ( $self, $data, $filename, $summary, %opts ) = @_;
60
61
    return $self->{client}->add_attachment( $self->id, $data, $filename, $summary, %opts );
62
}
63
64
sub obsolete_attachment {
65
    my ( $self, $attachment_id ) = @_;
66
67
    return $self->{client}->obsolete_attachment($attachment_id);
68
}
69
70
1;
(-)a/lib/GitBz/Commands/Attach.pm (-27 / +131 lines)
Lines 17-24 package GitBz::Commands::Attach; Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use utf8;
21
22
use Getopt::Long qw(GetOptionsFromArray);
20
use Getopt::Long qw(GetOptionsFromArray);
23
use Try::Tiny    qw(catch try);
21
use Try::Tiny    qw(catch try);
24
use File::Temp;
22
use File::Temp;
Lines 26-35 use File::Temp; Link Here
26
use GitBz::Git;
24
use GitBz::Git;
27
use GitBz::Exception;
25
use GitBz::Exception;
28
use GitBz::StatusWorkflow;
26
use GitBz::StatusWorkflow;
27
use GitBz::Bug;
29
28
30
sub new {
29
sub new {
31
    my ( $class, $commands ) = @_;
30
    my ( $class, $commands ) = @_;
32
    bless { commands => $commands }, $class;
31
    bless {
32
        commands => $commands,
33
        client   => $commands->{client}
34
    }, $class;
33
}
35
}
34
36
35
sub execute {
37
sub execute {
Lines 50-56 sub execute { Link Here
50
52
51
        $self->attach_patches( $bug_ref, \@commits, \%opts );
53
        $self->attach_patches( $bug_ref, \@commits, \%opts );
52
54
53
        print "Successfully attached " . scalar(@commits) . " patch(es) to bug $bug_ref\n";
55
        print "\n✓ Successfully attached " . scalar(@commits) . " patch(es) to bug $bug_ref\n";
54
    } catch {
56
    } catch {
55
        GitBz::Exception->throw("Attach failed: $_");
57
        GitBz::Exception->throw("Attach failed: $_");
56
    };
58
    };
Lines 90-99 sub extract_bug_ref { Link Here
90
sub attach_patches {
92
sub attach_patches {
91
    my ( $self, $bug_ref, $commits, $opts ) = @_;
93
    my ( $self, $bug_ref, $commits, $opts ) = @_;
92
94
93
    my $client = $self->{commands}->{client};
95
    my $client = $self->{client};
94
    my $bug    = $client->get_bug($bug_ref);
96
    my $bug    = GitBz::Bug->get( $client, $bug_ref );
95
97
96
    my $is_first = 1;
98
    my $is_first = 1;
99
    my %bug_updates;
100
    my @obsoletes_list;
101
97
    for my $commit (@$commits) {
102
    for my $commit (@$commits) {
98
        my $patch       = GitBz::Git->format_patch( $commit->{id} . '^..' . $commit->{id} );
103
        my $patch       = GitBz::Git->format_patch( $commit->{id} . '^..' . $commit->{id} );
99
        my $filename    = sprintf( "%s.patch", substr( $commit->{id}, 0, 7 ) );
104
        my $filename    = sprintf( "%s.patch", substr( $commit->{id}, 0, 7 ) );
Lines 103-131 sub attach_patches { Link Here
103
        my @obsoletes;
108
        my @obsoletes;
104
109
105
        if ( $opts->{edit} && $is_first ) {
110
        if ( $opts->{edit} && $is_first ) {
106
            ( $description, $comment, my $obsoletes_ref ) = $self->edit_attachment_comment( $bug, $commit );
111
            ( $description, $comment, my $obsoletes_ref, my $updates_ref ) =
107
            @obsoletes = @$obsoletes_ref if $obsoletes_ref;
112
                $self->edit_attachment_comment( $bug, $commit );
113
            @obsoletes   = @$obsoletes_ref if $obsoletes_ref;
114
            %bug_updates = %$updates_ref   if $updates_ref;
115
            push @obsoletes_list, @obsoletes;
108
        }
116
        }
109
117
110
        $client->add_attachment(
118
        $bug->add_attachment(
111
            $bug_ref,
112
            $patch,
119
            $patch,
113
            $filename,
120
            $filename,
114
            $description,
121
            $description,
115
            comment   => $comment,
122
            comment => $comment,
116
            obsoletes => \@obsoletes
117
        );
123
        );
118
124
119
        print "Attached: $description\n";
125
        # Store attachment info for later display
126
        push @{ $self->{_attached} }, $description;
120
        $is_first = 0;
127
        $is_first = 0;
121
    }
128
    }
129
130
    # Show all updates together
131
    if ( %bug_updates || @obsoletes_list || $self->{_attached} ) {
132
        print "\nUpdating bug $bug_ref:\n";
133
134
        # Show attachments
135
        for my $desc ( @{ $self->{_attached} || [] } ) {
136
            print "  ✓ Attached: $desc\n";
137
        }
138
139
        # Show bug field updates
140
        if ( $bug_updates{status} && $bug_updates{status} ne $bug->status ) {
141
            print "  ✓ Status: " . $bug->status . " → $bug_updates{status}\n";
142
        }
143
        if (   $bug_updates{cf_patch_complexity}
144
            && $bug_updates{cf_patch_complexity} ne ( $bug->cf_patch_complexity || '' ) )
145
        {
146
            my $old = $bug->cf_patch_complexity || 'none';
147
            print "  ✓ Patch-complexity: $old → $bug_updates{cf_patch_complexity}\n";
148
        }
149
        if ( $bug_updates{comment} ) {
150
            print "  ✓ Added comment\n";
151
        }
152
        if ( $bug_updates{depends_on} ) {
153
            my $depends_change = $bug_updates{depends_on};
154
            if ( $depends_change->{add} ) {
155
                print "  ✓ Depends: added " . join( ' ', @{ $depends_change->{add} } ) . "\n";
156
            }
157
            if ( $depends_change->{remove} ) {
158
                print "  ✓ Depends: removed " . join( ' ', @{ $depends_change->{remove} } ) . "\n";
159
            }
160
        }
161
162
        # Show obsoleted attachments
163
        for my $attach_id (@obsoletes_list) {
164
            my $attachments   = $bug->attachments;
165
            my %attach_lookup = map { $_->{id} => $_->{summary} } @$attachments;
166
            my $summary       = $attach_lookup{$attach_id} || "Unknown";
167
            print "  ✓ Obsoleted attachment $attach_id - $summary\n";
168
        }
169
170
        # Perform updates
171
        if (%bug_updates) {
172
            $bug->update(%bug_updates);
173
        }
174
175
        # Perform obsoletes
176
        for my $attach_id (@obsoletes_list) {
177
            $bug->obsolete_attachment($attach_id);
178
        }
179
    }
180
181
    # Obsoletes are now handled in the unified update section above
122
}
182
}
123
183
124
sub edit_attachment_comment {
184
sub edit_attachment_comment {
125
    my ( $self, $bug, $commit ) = @_;
185
    my ( $self, $bug, $commit ) = @_;
126
186
187
    my $client = $self->{client};
188
127
    my $template = "";
189
    my $template = "";
128
    $template .= "# Attachment to Bug $bug->{id} - $bug->{summary}\n\n";
190
    $template .= "# Attachment to Bug " . $bug->id . " - " . $bug->summary . "\n\n";
129
    $template .= $commit->{subject} . "\n\n";
191
    $template .= $commit->{subject} . "\n\n";
130
192
131
    # Add commit body as initial comment
193
    # Add commit body as initial comment
Lines 133-141 sub edit_attachment_comment { Link Here
133
    $template .= $body . "\n\n" if $body;
195
    $template .= $body . "\n\n" if $body;
134
196
135
    # Show existing patches for obsoleting
197
    # Show existing patches for obsoleting
136
    if ( $bug->{attachments} && @{ $bug->{attachments} } ) {
198
    my $attachments = $bug->attachments;
137
        for my $patch ( @{ $bug->{attachments} } ) {
199
    if ( $attachments && @$attachments ) {
138
            next unless $patch->{is_patch};
200
        for my $patch (@$attachments) {
201
            next unless $patch->{is_patch} && !$patch->{is_obsolete};
139
            my $obsoleted = ( $commit->{subject} eq $patch->{summary} ) ? "" : "#";
202
            my $obsoleted = ( $commit->{subject} eq $patch->{summary} ) ? "" : "#";
140
            $template .= "${obsoleted}Obsoletes: $patch->{id} - $patch->{summary}\n";
203
            $template .= "${obsoleted}Obsoletes: $patch->{id} - $patch->{summary}\n";
141
        }
204
        }
Lines 143-159 sub edit_attachment_comment { Link Here
143
    }
206
    }
144
207
145
    # Add status options
208
    # Add status options
146
    my $client   = $self->{commands}->{client};
147
    my $workflow = GitBz::StatusWorkflow->new($client);
209
    my $workflow = GitBz::StatusWorkflow->new($client);
148
    $template .= "# Current status: $bug->{status}\n";
210
    $template .= "# Current status: " . $bug->status . "\n";
149
    my $status_values = $workflow->get_next_status_values( $bug->{status} );
211
    my $status_values = $workflow->get_next_status_values( $bug->status );
150
    for my $status (@$status_values) {
212
    for my $status (@$status_values) {
151
        $template .= "# Status: $status\n";
213
        $template .= "# Status: $status\n";
152
    }
214
    }
153
    $template .= "\n";
215
    $template .= "\n";
154
216
155
    # Add patch complexity options
217
    # Add patch complexity options
156
    my $complexity = $bug->{cf_patch_complexity} || "";
218
    my $complexity = $bug->cf_patch_complexity || "";
157
    $template .= "# Current patch-complexity: $complexity\n";
219
    $template .= "# Current patch-complexity: $complexity\n";
158
    my $complexity_values = $client->get_field_values('cf_patch_complexity');
220
    my $complexity_values = $client->get_field_values('cf_patch_complexity');
159
    if ($complexity_values) {
221
    if ($complexity_values) {
Lines 164-172 sub edit_attachment_comment { Link Here
164
    $template .= "\n";
226
    $template .= "\n";
165
227
166
    # Add depends options
228
    # Add depends options
167
    my $depends     = $bug->{depends_on} || [];
229
    my $depends_list = $bug->depends_on;
168
    my $depends_str = ref($depends) eq 'ARRAY' ? join( ' ', @$depends ) : $depends;
230
    my $depends_str  = @$depends_list ? join( ' ', @$depends_list ) : '';
169
    $template .= "# Current depends: $depends_str\n";
231
    $template .= "# Current depends: $depends_str\n";
232
233
    # Show current depends uncommented
234
    for my $dep (@$depends_list) {
235
        $template .= "Depends: bug $dep\n";
236
    }
237
238
    # Show skeleton commented
170
    $template .= "# Depends: bug xxxx\n";
239
    $template .= "# Depends: bug xxxx\n";
171
    $template .= "# Depends: bug yyyy\n";
240
    $template .= "# Depends: bug yyyy\n";
172
    $template .= "\n";
241
    $template .= "\n";
Lines 176-182 sub edit_attachment_comment { Link Here
176
    $template .= "# To obsolete existing patches, uncomment the appropriate lines.\n";
245
    $template .= "# To obsolete existing patches, uncomment the appropriate lines.\n";
177
246
178
    my $edited = $self->edit_template($template);
247
    my $edited = $self->edit_template($template);
179
    return $self->parse_edited_content($edited);
248
    return $self->parse_edited_content( $edited, $bug );
180
}
249
}
181
250
182
sub edit_template {
251
sub edit_template {
Lines 198-217 sub edit_template { Link Here
198
}
267
}
199
268
200
sub parse_edited_content {
269
sub parse_edited_content {
201
    my ( $self, $content ) = @_;
270
    my ( $self, $content, $bug ) = @_;
202
271
203
    my @lines = split /\n/, $content;
272
    my @lines = split /\n/, $content;
204
    my @non_comment_lines = grep { !/^#/ && /\S/ } @lines;    # Also filter empty lines
273
    my @non_comment_lines = grep { !/^#/ && /\S/ } @lines;
205
274
206
    my $description = shift @non_comment_lines || "";
275
    my $description = shift @non_comment_lines || "";
207
    $description =~ s/^\s+|\s+$//g;
276
    $description =~ s/^\s+|\s+$//g;
208
277
209
    my @obsoletes;
278
    my @obsoletes;
210
    my @comment_lines;
279
    my @comment_lines;
280
    my %bug_updates;
211
281
212
    for my $line (@non_comment_lines) {
282
    for my $line (@non_comment_lines) {
213
        if ( $line =~ /^\s*Obsoletes\s*:\s*(\d+)/ ) {
283
        if ( $line =~ /^\s*Obsoletes\s*:\s*(\d+)/ ) {
214
            push @obsoletes, $1;
284
            push @obsoletes, $1;
285
        } elsif ( $line =~ /^\s*Status\s*:\s*(.+)/ ) {
286
            $bug_updates{status} = $1;
287
        } elsif ( $line =~ /^\s*Patch-complexity\s*:\s*(.+)/ ) {
288
            $bug_updates{cf_patch_complexity} = $1;
289
        } elsif ( $line =~ /^\s*Depends\s*:\s*([Bb][Uu][Gg])?\s*(\d+)/ ) {
290
            push @{ $bug_updates{depends_on} }, $2;
215
        } else {
291
        } else {
216
            push @comment_lines, $line;
292
            push @comment_lines, $line;
217
        }
293
        }
Lines 220-228 sub parse_edited_content { Link Here
220
    my $comment = join( "\n", @comment_lines );
296
    my $comment = join( "\n", @comment_lines );
221
    $comment =~ s/^\s+|\s+$//g;
297
    $comment =~ s/^\s+|\s+$//g;
222
298
299
    if ($comment) {
300
        $bug_updates{comment} = { body => $comment };
301
    }
302
303
    # Convert depends_on to proper API format with add/remove actions
304
    if ( $bug_updates{depends_on} ) {
305
        my @new_depends = @{ $bug_updates{depends_on} };
306
        my @old_depends = @{ $bug->depends_on };
307
308
        my @to_add = grep {
309
            my $new = $_;
310
            !grep { $_ eq $new } @old_depends
311
        } @new_depends;
312
        my @to_remove = grep {
313
            my $old = $_;
314
            !grep { $_ eq $old } @new_depends
315
        } @old_depends;
316
317
        if ( @to_add || @to_remove ) {
318
            my %depends_update;
319
            $depends_update{add}     = \@to_add    if @to_add;
320
            $depends_update{remove}  = \@to_remove if @to_remove;
321
            $bug_updates{depends_on} = \%depends_update;
322
        } else {
323
            delete $bug_updates{depends_on};
324
        }
325
    }
326
223
    GitBz::Exception->throw("Empty description, aborting\n") unless $description;
327
    GitBz::Exception->throw("Empty description, aborting\n") unless $description;
224
328
225
    return ( $description, $comment, \@obsoletes );
329
    return ( $description, $comment, \@obsoletes, \%bug_updates );
226
}
330
}
227
331
228
1;
332
1;
(-)a/lib/GitBz/Commands/Edit.pm (-54 / +86 lines)
Lines 17-30 package GitBz::Commands::Edit; Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use utf8;
21
22
use Getopt::Long qw(GetOptionsFromArray);
20
use Getopt::Long qw(GetOptionsFromArray);
23
use Try::Tiny    qw(catch try);
21
use Try::Tiny    qw(catch try);
24
use File::Temp;
22
use File::Temp;
25
use GitBz::Git;
23
use GitBz::Git;
26
use GitBz::Exception;
24
use GitBz::Exception;
27
use GitBz::StatusWorkflow;
25
use GitBz::StatusWorkflow;
26
use GitBz::Bug;
28
27
29
sub new {
28
sub new {
30
    my ( $class, $commands ) = @_;
29
    my ( $class, $commands ) = @_;
Lines 76-83 sub edit_bug { Link Here
76
    my ( $self, $bug_ref, $opts ) = @_;
75
    my ( $self, $bug_ref, $opts ) = @_;
77
76
78
    my $client = $self->{commands}->{client};
77
    my $client = $self->{commands}->{client};
79
    my $bug    = $client->get_bug($bug_ref);
78
    my $bug    = GitBz::Bug->get( $client, $bug_ref );
80
    GitBz::Exception->throw("Bug $bug_ref not found") unless $bug;
81
79
82
    my $template = $self->create_bug_template( $bug, $opts );
80
    my $template = $self->create_bug_template( $bug, $opts );
83
    my $edited   = $self->edit_template($template);
81
    my $edited   = $self->edit_template($template);
Lines 106-112 sub edit_commits { Link Here
106
        my $changed = $self->edit_bug_with_commits( $bug_ref, $bugs{$bug_ref}, $opts );
104
        my $changed = $self->edit_bug_with_commits( $bug_ref, $bugs{$bug_ref}, $opts );
107
        $any_changed ||= $changed;
105
        $any_changed ||= $changed;
108
    }
106
    }
109
    
107
110
    return $any_changed;
108
    return $any_changed;
111
}
109
}
112
110
Lines 114-121 sub edit_bug_with_commits { Link Here
114
    my ( $self, $bug_ref, $commits, $opts ) = @_;
112
    my ( $self, $bug_ref, $commits, $opts ) = @_;
115
113
116
    my $client = $self->{commands}->{client};
114
    my $client = $self->{commands}->{client};
117
    my $bug    = $client->get_bug($bug_ref);
115
    my $bug    = GitBz::Bug->get( $client, $bug_ref );
118
    GitBz::Exception->throw("Bug $bug_ref not found") unless $bug;
119
116
120
    my $template = $self->create_bug_template_with_commits( $bug, $commits, $opts );
117
    my $template = $self->create_bug_template_with_commits( $bug, $commits, $opts );
121
    my $edited   = $self->edit_template($template);
118
    my $edited   = $self->edit_template($template);
Lines 128-156 sub create_bug_template { Link Here
128
    my $client = $self->{commands}->{client};
125
    my $client = $self->{commands}->{client};
129
126
130
    my $template = "";
127
    my $template = "";
131
    $template .= "# Bug $bug->{id} - $bug->{summary}\n\n";
128
    $template .= "# Bug " . $bug->id . " - " . $bug->summary . "\n\n";
132
    
129
133
    # Show existing patches for obsoleting
130
    # Show existing patches for obsoleting
134
    my $attachments = $client->get_attachments($bug->{id});
131
    my $attachments = $bug->attachments;
135
    if ( $attachments && @$attachments ) {
132
    if ( $attachments && @$attachments ) {
136
        for my $patch ( @$attachments ) {
133
        for my $patch (@$attachments) {
137
            next unless $patch->{is_patch} && !$patch->{is_obsolete};
134
            next unless $patch->{is_patch} && !$patch->{is_obsolete};
138
            $template .= "#Obsoletes: $patch->{id} - $patch->{summary}\n";
135
            $template .= "#Obsoletes: $patch->{id} - $patch->{summary}\n";
139
        }
136
        }
140
        $template .= "\n";
137
        $template .= "\n";
141
    }
138
    }
142
    
139
143
    # Add status options
140
    # Add status options
144
    my $workflow = GitBz::StatusWorkflow->new($client);
141
    my $workflow = GitBz::StatusWorkflow->new($client);
145
    $template .= "# Current status: $bug->{status}\n";
142
    $template .= "# Current status: " . $bug->status . "\n";
146
    my $status_values = $workflow->get_next_status_values($bug->{status});
143
    my $status_values = $workflow->get_next_status_values( $bug->status );
147
    for my $status (@$status_values) {
144
    for my $status (@$status_values) {
148
        $template .= "# Status: $status\n";
145
        $template .= "# Status: $status\n";
149
    }
146
    }
150
    $template .= "\n";
147
    $template .= "\n";
151
    
148
152
    # Add patch complexity options
149
    # Add patch complexity options
153
    my $complexity = $bug->{cf_patch_complexity} || "";
150
    my $complexity = $bug->cf_patch_complexity || "";
154
    $template .= "# Current patch-complexity: $complexity\n";
151
    $template .= "# Current patch-complexity: $complexity\n";
155
    my $complexity_values = $client->get_field_values('cf_patch_complexity');
152
    my $complexity_values = $client->get_field_values('cf_patch_complexity');
156
    if ($complexity_values) {
153
    if ($complexity_values) {
Lines 159-173 sub create_bug_template { Link Here
159
        }
156
        }
160
    }
157
    }
161
    $template .= "\n";
158
    $template .= "\n";
162
    
159
163
    # Add depends options
160
    # Add depends options
164
    my $depends = $bug->{depends_on} || [];
161
    my $depends_list = $bug->depends_on;
165
    my $depends_str = ref($depends) eq 'ARRAY' ? join(' ', @$depends) : $depends;
162
    my $depends_str  = @$depends_list ? join( ' ', @$depends_list ) : '';
166
    $template .= "# Current depends: $depends_str\n";
163
    $template .= "# Current depends: $depends_str\n";
164
165
    # Show current depends uncommented
166
    for my $dep (@$depends_list) {
167
        $template .= "Depends: bug $dep\n";
168
    }
169
170
    # Show skeleton commented
167
    $template .= "# Depends: bug xxxx\n";
171
    $template .= "# Depends: bug xxxx\n";
168
    $template .= "# Depends: bug yyyy\n";
172
    $template .= "# Depends: bug yyyy\n";
169
    $template .= "\n";
173
    $template .= "\n";
170
    
174
171
    $template .= "# Enter comment below. Lines starting with '#' will be ignored.\n";
175
    $template .= "# Enter comment below. Lines starting with '#' will be ignored.\n";
172
    $template .= "# To obsolete patches, uncomment the appropriate Obsoletes lines.\n";
176
    $template .= "# To obsolete patches, uncomment the appropriate Obsoletes lines.\n";
173
177
Lines 217-230 sub update_bug { Link Here
217
221
218
    my @lines = split /\n/, $edited;
222
    my @lines = split /\n/, $edited;
219
    my @non_comment_lines = grep { !/^#/ && /\S/ } @lines;
223
    my @non_comment_lines = grep { !/^#/ && /\S/ } @lines;
220
    
224
221
    # Early return if no non-comment lines - no API calls needed
225
    # Early return if no non-comment lines - no API calls needed
222
    return 0 unless @non_comment_lines;
226
    return 0 unless @non_comment_lines;
223
    
227
224
    my @obsoletes;
228
    my @obsoletes;
225
    my @comment_lines;
229
    my @comment_lines;
226
    my %update_params;
230
    my %update_params;
227
    
231
228
    for my $line (@non_comment_lines) {
232
    for my $line (@non_comment_lines) {
229
        if ( $line =~ /^\s*Status\s*:\s*(.+)/ ) {
233
        if ( $line =~ /^\s*Status\s*:\s*(.+)/ ) {
230
            $update_params{status} = $1;
234
            $update_params{status} = $1;
Lines 240-310 sub update_bug { Link Here
240
            push @comment_lines, $line;
244
            push @comment_lines, $line;
241
        }
245
        }
242
    }
246
    }
243
    
247
244
    my $comment = join( "\n", @comment_lines );
248
    my $comment = join( "\n", @comment_lines );
245
    $comment =~ s/^\s+|\s+$//g;
249
    $comment =~ s/^\s+|\s+$//g;
246
    
250
247
    # Early return if no changes
251
    # Early return if no changes
248
    return 0 unless %update_params || $comment || @obsoletes;
252
    return 0 unless %update_params || $comment || @obsoletes;
249
    
253
250
    # Only fetch bug data if we have changes to process
254
    # Only fetch bug data if we have changes to process
251
    my $bug = $client->get_bug($bug_ref);
255
    my $bug     = GitBz::Bug->get( $client, $bug_ref );
252
    my $changed = 0;
256
    my $changed = 0;
253
    
257
254
    # Check if there are actual changes before making API call
258
    # Check if there are actual changes before making API call
255
    my %actual_changes;
259
    my %actual_changes;
256
    if ($update_params{status} && $update_params{status} ne $bug->{status}) {
260
    if ( $update_params{status} && $update_params{status} ne $bug->status ) {
257
        $actual_changes{status} = $update_params{status};
261
        $actual_changes{status} = $update_params{status};
258
    }
262
    }
259
    if ($update_params{resolution} && $update_params{resolution} ne ($bug->{resolution} || '')) {
263
    if ( $update_params{resolution} && $update_params{resolution} ne ( $bug->resolution || '' ) ) {
260
        $actual_changes{resolution} = $update_params{resolution};
264
        $actual_changes{resolution} = $update_params{resolution};
261
    }
265
    }
262
    if ($update_params{cf_patch_complexity} && $update_params{cf_patch_complexity} ne ($bug->{cf_patch_complexity} || '')) {
266
    if (   $update_params{cf_patch_complexity}
267
        && $update_params{cf_patch_complexity} ne ( $bug->cf_patch_complexity || '' ) )
268
    {
263
        $actual_changes{cf_patch_complexity} = $update_params{cf_patch_complexity};
269
        $actual_changes{cf_patch_complexity} = $update_params{cf_patch_complexity};
264
    }
270
    }
265
    if ($update_params{depends_on}) {
271
    if ( $update_params{depends_on} ) {
266
        $actual_changes{depends_on} = $update_params{depends_on};
272
        my @new_depends = @{ $update_params{depends_on} };
273
        my @old_depends = @{ $bug->depends_on };
274
275
        my @to_add = grep {
276
            my $new = $_;
277
            !grep { $_ eq $new } @old_depends
278
        } @new_depends;
279
        my @to_remove = grep {
280
            my $old = $_;
281
            !grep { $_ eq $old } @new_depends
282
        } @old_depends;
283
284
        if ( @to_add || @to_remove ) {
285
            my %depends_update;
286
            $depends_update{add}        = \@to_add    if @to_add;
287
            $depends_update{remove}     = \@to_remove if @to_remove;
288
            $actual_changes{depends_on} = \%depends_update;
289
        }
267
    }
290
    }
268
    if ($comment) {
291
    if ($comment) {
269
        $actual_changes{comment} = { body => $comment };
292
        $actual_changes{comment} = { body => $comment };
270
    }
293
    }
271
    
294
272
    if (%actual_changes) {
295
    if (%actual_changes) {
273
        print "Updating bug $bug_ref\n";
296
        print "Updating bug $bug_ref:\n";
274
        
297
275
        # Show field changes
298
        # Show field changes
276
        if ($actual_changes{status}) {
299
        if ( $actual_changes{status} ) {
277
            print "Status changed: $bug->{status} → $actual_changes{status}\n";
300
            print "  ✓ Status: " . $bug->status . " → $actual_changes{status}\n";
301
        }
302
        if ( $actual_changes{resolution} ) {
303
            my $old = $bug->resolution || 'none';
304
            print "  ✓ Resolution: $old → $actual_changes{resolution}\n";
278
        }
305
        }
279
        if ($actual_changes{resolution}) {
306
        if ( $actual_changes{cf_patch_complexity} ) {
280
            my $old = $bug->{resolution} || 'none';
307
            my $old = $bug->cf_patch_complexity || 'none';
281
            print "Resolution changed: $old → $actual_changes{resolution}\n";
308
            print "  ✓ Patch-complexity: $old → $actual_changes{cf_patch_complexity}\n";
282
        }
309
        }
283
        if ($actual_changes{cf_patch_complexity}) {
310
        if ( $actual_changes{comment} ) {
284
            my $old = $bug->{cf_patch_complexity} || 'none';
311
            print "  ✓ Added comment\n";
285
            print "Patch-complexity changed: $old → $actual_changes{cf_patch_complexity}\n";
286
        }
312
        }
287
        if ($actual_changes{comment}) {
313
        if ( $actual_changes{depends_on} ) {
288
            print "Added comment\n";
314
            my $depends_change = $actual_changes{depends_on};
315
            if ( $depends_change->{add} ) {
316
                print "  ✓ Depends: added " . join( ' ', @{ $depends_change->{add} } ) . "\n";
317
            }
318
            if ( $depends_change->{remove} ) {
319
                print "  ✓ Depends: removed " . join( ' ', @{ $depends_change->{remove} } ) . "\n";
320
            }
289
        }
321
        }
290
        
322
291
        $client->update_bug( $bug_ref, %actual_changes );
323
        $bug->update(%actual_changes);
292
        $changed = 1;
324
        $changed = 1;
293
    }
325
    }
294
    
326
295
    # Handle obsoleting attachments
327
    # Handle obsoleting attachments
296
    if (@obsoletes) {
328
    if (@obsoletes) {
297
        my $attachments = $client->get_attachments($bug_ref);
329
        my $attachments   = $bug->attachments;
298
        my %attach_lookup = map { $_->{id} => $_->{summary} } @$attachments;
330
        my %attach_lookup = map { $_->{id} => $_->{summary} } @$attachments;
299
        
331
300
        for my $attach_id (@obsoletes) {
332
        for my $attach_id (@obsoletes) {
301
            $client->obsolete_attachment($attach_id);
333
            $bug->obsolete_attachment($attach_id);
302
            my $summary = $attach_lookup{$attach_id} || "Unknown";
334
            my $summary = $attach_lookup{$attach_id} || "Unknown";
303
            print "Obsoleted attachment $attach_id - $summary\n";
335
            print "  ✓ Obsoleted attachment $attach_id - $summary\n";
304
            $changed = 1;
336
            $changed = 1;
305
        }
337
        }
306
    }
338
    }
307
    
339
308
    return $changed;
340
    return $changed;
309
}
341
}
310
342
(-)a/lib/GitBz/Git.pm (-1 / +6 lines)
Lines 16-21 package GitBz::Git; Link Here
16
# along with git-bz; if not, see <https://www.gnu.org/licenses>.
16
# along with git-bz; if not, see <https://www.gnu.org/licenses>.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
20
use Encode qw(decode);
19
use IPC::Run3;
21
use IPC::Run3;
20
use Try::Tiny qw(catch try);
22
use Try::Tiny qw(catch try);
21
use GitBz::Exception;
23
use GitBz::Exception;
Lines 31-37 sub run { Link Here
31
        if ( $? != 0 ) {
33
        if ( $? != 0 ) {
32
            GitBz::Exception::Git->throw("Git command failed: $stderr");
34
            GitBz::Exception::Git->throw("Git command failed: $stderr");
33
        }
35
        }
34
        chomp $stdout if $stdout;
36
        if ($stdout) {
37
            $stdout = decode( 'UTF-8', $stdout, Encode::FB_CROAK | Encode::LEAVE_SRC );
38
            chomp $stdout;
39
        }
35
        return $stdout || '';
40
        return $stdout || '';
36
    } catch {
41
    } catch {
37
        GitBz::Exception::Git->throw($_);
42
        GitBz::Exception::Git->throw($_);
(-)a/lib/GitBz/RestClient.pm (-11 / +9 lines)
Lines 16-22 package GitBz::RestClient; Link Here
16
# along with git-bz; if not, see <https://www.gnu.org/licenses>.
16
# along with git-bz; if not, see <https://www.gnu.org/licenses>.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
20
use LWP::UserAgent;
19
use LWP::UserAgent;
21
use JSON;
20
use JSON;
22
use MIME::Base64;
21
use MIME::Base64;
Lines 71-84 sub login { Link Here
71
70
72
sub get_token {
71
sub get_token {
73
    my ($self) = @_;
72
    my ($self) = @_;
74
    
73
75
    return $self->{token} if $self->{token};
74
    return $self->{token} if $self->{token};
76
    
75
77
    if ($self->{username} && $self->{password}) {
76
    if ( $self->{username} && $self->{password} ) {
78
        $self->login();
77
        $self->login();
79
        return $self->{token};
78
        return $self->{token};
80
    }
79
    }
81
    
80
82
    return undef;
81
    return undef;
83
}
82
}
84
83
Lines 170-176 sub update_bug { Link Here
170
sub get_field_values {
169
sub get_field_values {
171
    my ( $self, $field_name ) = @_;
170
    my ( $self, $field_name ) = @_;
172
171
173
    my $url = sprintf( "%s/field/bug", $self->{base_url} );
172
    my $url      = sprintf( "%s/field/bug", $self->{base_url} );
174
    my $response = $self->{ua}->get($url);
173
    my $response = $self->{ua}->get($url);
175
174
176
    if ( !$response->is_success ) {
175
    if ( !$response->is_success ) {
Lines 178-191 sub get_field_values { Link Here
178
    }
177
    }
179
178
180
    my $data = decode_json( $response->content );
179
    my $data = decode_json( $response->content );
181
    
180
182
    # Find the field in the fields array
181
    # Find the field in the fields array
183
    for my $field ( @{ $data->{fields} || [] } ) {
182
    for my $field ( @{ $data->{fields} || [] } ) {
184
        if ( $field->{name} eq $field_name && $field->{values} ) {
183
        if ( $field->{name} eq $field_name && $field->{values} ) {
185
            return [ map { $_->{name} } @{ $field->{values} } ];
184
            return [ map { $_->{name} } @{ $field->{values} } ];
186
        }
185
        }
187
    }
186
    }
188
    
187
189
    return [];
188
    return [];
190
}
189
}
191
190
Lines 195-201 sub obsolete_attachment { Link Here
195
    my $url = sprintf( "%s/bug/attachment/%s", $self->{base_url}, $attachment_id );
194
    my $url = sprintf( "%s/bug/attachment/%s", $self->{base_url}, $attachment_id );
196
195
197
    my $payload = {
196
    my $payload = {
198
        ids => [$attachment_id],
197
        ids         => [$attachment_id],
199
        is_obsolete => JSON::true,
198
        is_obsolete => JSON::true,
200
    };
199
    };
201
200
Lines 215-218 sub obsolete_attachment { Link Here
215
    return decode_json( $response->content );
214
    return decode_json( $response->content );
216
}
215
}
217
216
218
1;
217
1;
219
- 

Return to bug 6473