From b73d66d2a4a57e669d302bf8cd05961b38ac7246 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Wed, 24 Dec 2025 10:34:16 +0000 Subject: [PATCH] [#36] Add iterative error handling for invalid QA contacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user enters an invalid QA contact email (e.g., a typo or non-existent user), the bug update would fail with a 404 error and the spinner would hang indefinitely. This commit adds comprehensive error handling with user search and iterative retry logic. Changes: - Added search_users() method to RestClient.pm to query Bugzilla's user API endpoint with authentication token - Added select_qa_contact() method to Edit.pm and Attach.pm that: * Extracts the local part of the invalid email (before @) * Searches for similar users whose emails begin with that pattern * Presents a numbered list of matching users for selection * Allows manual entry if no matches found - Implemented iterative retry loop in both Edit and Attach commands: * On 404 error, searches for similar users * User can select from results or enter a different email * If manually entered email also fails, searches again * Continues until successful update or user cancels * Properly stops spinner on each error before continuing Example flow: 1. User enters "martinrenvoize@theke.io" → 404 2. Searches for "martinrenvoize" → no results 3. User enters "martin.renvoize@theke.io" → 404 4. Searches for "martin.renvoize" → finds matches 5. User selects "martin.renvoize@openfifth.co.uk" → success This provides a much better user experience than the previous behavior of hanging indefinitely with no feedback. --- lib/GitBz/Commands/Attach.pm | 131 +++++++++++++++++++++++++++++++++- lib/GitBz/Commands/Edit.pm | 134 ++++++++++++++++++++++++++++++++++- lib/GitBz/RestClient.pm | 41 ++++++++++- 3 files changed, 300 insertions(+), 6 deletions(-) diff --git a/lib/GitBz/Commands/Attach.pm b/lib/GitBz/Commands/Attach.pm index 4c29e7d..a9df70b 100644 --- a/lib/GitBz/Commands/Attach.pm +++ b/lib/GitBz/Commands/Attach.pm @@ -406,8 +406,68 @@ sub attach_patches { # Apply updates with spinner if (%bug_updates) { my $spinner = GitBz::Progress::start_spinner("Updating bug fields"); - $bug->apply_updates(); - GitBz::Progress::stop_spinner( $spinner, 'success', "Bug fields updated" ); + + my $update_success = eval { + $bug->apply_updates(); + 1; + }; + + if ($@) { + my $error = $@; + GitBz::Progress::stop_spinner( $spinner, 'error' ); + + # Check if it's a 404 error related to QA contact + if ( exists $bug_updates{qa_contact} && $error =~ /404|not found/i ) { + print "\n"; + GitBz::Progress::print_error("QA contact '$bug_updates{qa_contact}' not found"); + + # Iteratively search and retry until success or user cancels + my $attempted_email = $bug_updates{qa_contact}; + my $retry_success = 0; + + while ( !$retry_success ) { + # Offer to search for similar users + my $selected_email = $self->select_qa_contact( $client, $attempted_email ); + + if ( !$selected_email ) { + die "Update cancelled by user\n"; + } + + # Retry with the selected user + $bug->set_field( 'qa_contact', $selected_email ); + print "\nRetrying with QA contact: $selected_email\n"; + $spinner = GitBz::Progress::start_spinner("Updating bug fields"); + + my $retry_eval = eval { + $bug->apply_updates(); + 1; + }; + + if ($@) { + my $retry_error = $@; + GitBz::Progress::stop_spinner( $spinner, 'error' ); + + # Check if it's another 404 error + if ( $retry_error =~ /404|not found/i ) { + GitBz::Progress::print_error("QA contact '$selected_email' not found"); + $attempted_email = $selected_email; # Use this as the next search term + # Loop continues + } else { + # Different error, rethrow + die $retry_error; + } + } else { + # Success! + GitBz::Progress::stop_spinner( $spinner, 'success', "Bug fields updated" ); + $retry_success = 1; + } + } + } else { + die $error; + } + } else { + GitBz::Progress::stop_spinner( $spinner, 'success', "Bug fields updated" ); + } } # Perform obsoletes with real-time feedback @@ -659,4 +719,71 @@ sub parse_bug_updates { return ( $bug_comment, \@obsoletes, $bug_updates ); } +=head2 select_qa_contact + + my $email = $attach->select_qa_contact($client, $invalid_email); + +Searches for users matching the invalid email and presents a selection menu. +Returns the selected email, or undef if cancelled. + +=cut + +sub select_qa_contact { + my ( $self, $client, $invalid_email ) = @_; + + # Extract the local part (before @) from the invalid email + my $search_term; + if ( $invalid_email =~ /^([^@]+)@/ ) { + $search_term = $1; + } else { + $search_term = $invalid_email; + } + + # Search for users whose email begins with the local part + print "\nSearching for similar users (begins with '$search_term')...\n"; + my $users = GitBz::Progress::with_spinner( + "Searching users", + sub { + return $client->search_users($search_term); + }, + 1 + ); + + if ( !@$users ) { + print "No similar users found.\n"; + print "Would you like to enter a different QA contact? [y/N]: "; + my $response = ; + chomp $response; + + if ( $response =~ /^[yY]/ ) { + print "Enter QA contact email: "; + my $new_email = ; + chomp $new_email; + $new_email =~ s/^\s+|\s+$//g; + return $new_email if $new_email; + } + + return; + } + + # Present users for selection + print "\nFound " . scalar(@$users) . " similar user(s):\n\n"; + + for my $i ( 0 .. $#$users ) { + my $user = $users->[$i]; + my $display_name = $user->{real_name} ? "$user->{real_name} <$user->{email}>" : $user->{email}; + print " [$i] $display_name\n"; + } + + print "\nSelect a user by number, or press Enter to cancel: "; + my $selection = ; + chomp $selection; + + if ( $selection =~ /^\d+$/ && $selection >= 0 && $selection <= $#$users ) { + return $users->[$selection]{email}; + } + + return; +} + 1; diff --git a/lib/GitBz/Commands/Edit.pm b/lib/GitBz/Commands/Edit.pm index 6b31028..fbcc103 100644 --- a/lib/GitBz/Commands/Edit.pm +++ b/lib/GitBz/Commands/Edit.pm @@ -393,9 +393,70 @@ sub update_bug { $bug->_display_changes(); my $spinner = GitBz::Progress::start_spinner("Updating bug fields"); - $bug->apply_updates(); - GitBz::Progress::stop_spinner( $spinner, 'success', "Bug fields updated" ); - $changed = 1; + + my $update_success = eval { + $bug->apply_updates(); + 1; + }; + + if ($@) { + my $error = $@; + GitBz::Progress::stop_spinner( $spinner, 'error' ); + + # Check if it's a 404 error related to QA contact + if ( exists $actual_changes{qa_contact} && $error =~ /404|not found/i ) { + print "\n"; + GitBz::Progress::print_error("QA contact '$actual_changes{qa_contact}' not found"); + + # Iteratively search and retry until success or user cancels + my $attempted_email = $actual_changes{qa_contact}; + my $retry_success = 0; + + while ( !$retry_success ) { + # Offer to search for similar users + my $selected_email = $self->select_qa_contact( $client, $attempted_email ); + + if ( !$selected_email ) { + die "Update cancelled by user\n"; + } + + # Retry with the selected user + $bug->set_field( 'qa_contact', $selected_email ); + print "\nRetrying with QA contact: $selected_email\n"; + $spinner = GitBz::Progress::start_spinner("Updating bug fields"); + + my $retry_eval = eval { + $bug->apply_updates(); + 1; + }; + + if ($@) { + my $retry_error = $@; + GitBz::Progress::stop_spinner( $spinner, 'error' ); + + # Check if it's another 404 error + if ( $retry_error =~ /404|not found/i ) { + GitBz::Progress::print_error("QA contact '$selected_email' not found"); + $attempted_email = $selected_email; # Use this as the next search term + # Loop continues + } else { + # Different error, rethrow + die $retry_error; + } + } else { + # Success! + GitBz::Progress::stop_spinner( $spinner, 'success', "Bug fields updated" ); + $changed = 1; + $retry_success = 1; + } + } + } else { + die $error; + } + } else { + GitBz::Progress::stop_spinner( $spinner, 'success', "Bug fields updated" ); + $changed = 1; + } } # Handle obsoleting attachments @@ -445,4 +506,71 @@ sub extract_bug_ref { return; } +=head2 select_qa_contact + + my $email = $edit->select_qa_contact($client, $invalid_email); + +Searches for users matching the invalid email and presents a selection menu. +Returns the selected email, or undef if cancelled. + +=cut + +sub select_qa_contact { + my ( $self, $client, $invalid_email ) = @_; + + # Extract the local part (before @) from the invalid email + my $search_term; + if ( $invalid_email =~ /^([^@]+)@/ ) { + $search_term = $1; + } else { + $search_term = $invalid_email; + } + + # Search for users whose email begins with the local part + print "\nSearching for similar users (begins with '$search_term')...\n"; + my $users = GitBz::Progress::with_spinner( + "Searching users", + sub { + return $client->search_users($search_term); + }, + 1 + ); + + if ( !@$users ) { + print "No similar users found.\n"; + print "Would you like to enter a different QA contact? [y/N]: "; + my $response = ; + chomp $response; + + if ( $response =~ /^[yY]/ ) { + print "Enter QA contact email: "; + my $new_email = ; + chomp $new_email; + $new_email =~ s/^\s+|\s+$//g; + return $new_email if $new_email; + } + + return; + } + + # Present users for selection + print "\nFound " . scalar(@$users) . " similar user(s):\n\n"; + + for my $i ( 0 .. $#$users ) { + my $user = $users->[$i]; + my $display_name = $user->{real_name} ? "$user->{real_name} <$user->{email}>" : $user->{email}; + print " [$i] $display_name\n"; + } + + print "\nSelect a user by number, or press Enter to cancel: "; + my $selection = ; + chomp $selection; + + if ( $selection =~ /^\d+$/ && $selection >= 0 && $selection <= $#$users ) { + return $users->[$selection]{email}; + } + + return; +} + 1; diff --git a/lib/GitBz/RestClient.pm b/lib/GitBz/RestClient.pm index ed17db9..492528a 100644 --- a/lib/GitBz/RestClient.pm +++ b/lib/GitBz/RestClient.pm @@ -274,6 +274,45 @@ sub get_field_values { return []; } +=head2 search_users + + my $users = $client->search_users($search_term); + +Searches for users matching the given search term. +Returns arrayref of user hashes with 'email', 'real_name', and 'id' keys. + +=cut + +sub search_users { + my ( $self, $search_term ) = @_; + + return [] unless $search_term; + + # Get authentication token + my $token = $self->get_token(); + + # Build URL with token for authentication + my $url = sprintf( "%s/user?match=%s", $self->{base_url}, $search_term ); + $url .= "&token=$token" if $token; + + my $response = $self->{ua}->get($url); + + if ( !$response->is_success ) { + return []; + } + + my $data = decode_json( $response->content ); + + # Return users array with relevant fields + return [ map { + { + email => $_->{email}, + real_name => $_->{real_name} || '', + id => $_->{id} + } + } @{ $data->{users} || [] } ]; +} + =head2 obsolete_attachment $client->obsolete_attachment($attachment_id); @@ -353,4 +392,4 @@ sub get_bug_url { return "$web_url/show_bug.cgi?id=$bug_id"; } -1; \ No newline at end of file +1; -- 2.50.1 (Apple Git-155)