#!/usr/bin/env perl # # SPDX-License-Identifier: GPL-2.0-only package gerrit_stats; # To install any needed modules install the cpanm app, and use it to install the required modules: # sudo cpan App::cpanminus # sudo /usr/local/bin/cpanm JSON::Util Net::OpenSSH DateTime Devel::Size # perltidy -l=200 -bt=2 -ce use strict; use warnings; use English qw( -no_match_vars ); use File::Find; use File::Path; use Getopt::Long; use Getopt::Std; use JSON::Util; use Net::OpenSSH; use Data::Dumper qw(Dumper); use DateTime; use Devel::Size qw(size total_size); my $old_version; my $new_version; my $infodir = "$ENV{'HOME'}/.local/commit_info/" . `git config -l | grep remote.origin.url | sed 's|.*@||' | sed 's|:.*||'`; chomp($infodir); my $URL_WITH_USER; my $SKIP_GERRIT_CHECK; my $print_commit_list = 1; #disable print buffering $OUTPUT_AUTOFLUSH = 1; binmode STDOUT, ":utf8"; Main(); #------------------------------------------------------------------------------- # Main #------------------------------------------------------------------------------- sub Main { check_arguments(); my %submitters = (); my %authors = (); my %owners = (); my %reviewers = (); my %commenters = (); my %author_added = (); my %author_removed = (); my $total_added = 0; my $total_removed = 0; my $number_of_commits = 0; my $number_of_submitters = 0; my $submit_epoch = ""; my $first_submit_epoch = ""; my $patches_over_100_lines = 0; my $total_lines_large_patches = 0; my %email_addresses = ( 'Kacper Stojek' => 'kacper.stojek@3mdeb.com', 'Damien Zammit' => 'damien@zamaudio.com', 'Pavel Sayekat' => 'pavelsayekat@gmail.com', 'Lance Zhao' => 'lance.zhao@gmail.com', ); my %aliases = ( ' Felix Singer' => 'Felix Singer', 'Abhay kumar' => 'Abhay Kumar', 'AlexandruX Gagniuc' => 'Alexandru Gagniuc', 'Anish K. Patel' => 'Anish K Patel', 'Bao Zheng' => 'Zheng Bao', 'Bernhard M. Wiedemann' => 'Bernhard M. Wiedermann', 'Björn Busse' => 'Bjarn Busse', 'BryantOu' => 'Bryant Ou', 'Chen Wisley' => 'Wisley Chen', 'Cheng-Yi Chiang' => 'Jimmy Cheng-Yi Chiang', 'Chris Ching (using chromium account)' => 'Chris Ching,', 'ChromeOS Developer' => 'Dave Parker', 'Cristi M' => 'Cristian Magherusan-Stanciu', 'Cristian M?gheru?an-Stanciu' => 'Cristian Magherusan-Stanciu', 'Cristian MÄgheruÈan-Stanciu' => 'Cristian Magherusan-Stanciu', 'Cristian MÄgheruÈan-Stanciu' => 'Cristian Magherusan-Stanciu', 'DAWEI CHIEN' => 'Dawei Chien', 'efdesign98' => 'Frank Vibrans', 'Eugene D. Myers' => 'Eugene Myers', 'Frank Vibrans III' => 'Frank Vibrans', 'frank vibrans' => 'Frank Vibrans', 'Frank.Vibrans' => 'Frank Vibrans', 'FrankChu' => 'Frank Chu', 'garmin chang' => 'Garmin Chang', 'Garmin.Chang' => 'Garmin Chang', 'hannahwilliams2' => 'Hannah Williams', 'HAOUAS Elyes' => 'Elyes Haouas', 'Harshapriya N' => 'Harsha Priya', 'Hsuan-ting Chen' => 'Hsuan Ting Chen', 'Iru Cai (vimacs)' => 'Iru Cai', 'Jérémy Compostella' => 'Jeremy Compostella', 'Jérémy Compostella' => 'Jeremy Compostella', 'JG Poxu' => 'Po Xu', 'JonathonHall-Purism' => 'Jonathon Hall', 'Karthikeyan Ramasubramanian' => 'Karthik Ramasubramanian', 'Kerry She' => 'Kerry Sheh', 'kewei.xu' => 'kewei xu', 'Kumar, Gomathi' => 'Gomathi Kumar', 'Kyösti Mälkki' => 'Kyösti Mälkki', 'Kyösti Mälkki' => 'Kyösti Mälkki', 'Marcello Sylvester Bauer' => 'Marcello Sylvester Bauer', 'Martin L Roth' => 'Martin Roth', 'Martin Roth - Personal' => 'Martin Roth', 'Matt Ziegelbaum' => 'Matthew Ziegelbaum', 'mengqi.zhang' => 'Mengqi Zhang', 'mrnuke' => 'Alexandru Gagniuc', 'Nina-CM Wu' => 'Nina Wu', 'ot_zhenguo.li' => 'Zhenguo Li', 'Patrick Georgi patrick.georgi' => 'Patrick Georgi', 'Patrick Georgi patrick' => 'Patrick Georgi', 'Pavlushka' => 'Pavel Sayekat', 'Ravi Kumar Bokka' => 'Ravi Kumar', 'Ravi kumar' => 'Ravi Kumar', 'Ravi Sarawadi' => 'Ravishankar Sarawadi', 'ravindr1' => 'Ravindra', 'Ravindra N' => 'Ravindra', 'Ricardo Ribalda Delgado' => 'Ricardo Ribalda', 'ron minnich' => 'Ron Minnich', 'Ronald G. Minnich' => 'Ron Minnich', 'samrab' => 'Sudheer Amrabadi', 'SANTHOSH JANARDHANA HASSAN' => 'Santhosh Janardhana Hassan', 'semihalf-czapiga-jakub' => 'Jakub Czapiga', 'Seunghwan Kim' => 'SH Kim', 'Sooi, Li Cheng' => 'Li Cheng Sooi', 'Stefan Reinauerstepan' => 'Stefan Reinauer', 'stepan' => 'Stefan Reinauer', 'Swift Geek (Sebastian Grzywna)' => 'Sebastian "Swift Geek" Grzywna', 'Sylvain "ythier" Hitier' => 'Sylvain Hitier', 'Thomas Gstädtner' => 'Thomas Gstaedtner', 'UwePoeche' => 'Uwe Poeche', 'UwePoeche' => 'Uwe Poeche', 'Varshit Pandya' => 'Varshit B Pandya', 'Wayne3 Wang' => 'Wayne Wang', 'Wayne3_Wang' => 'Wayne Wang', 'Xi Chen' => 'Xixi Chen', 'Yu-Hsuan Hsu' => 'Yu-hsuan Hsu', 'zbao' => 'Zheng Bao', 'Zheng Bao zheng.bao' => 'Zheng Bao', 'zhiyong tao' => 'Zhiyong Tao', 'Дмитрий Понаморев' => 'Dmitry Ponamorev', ); if ( !$URL_WITH_USER ) { get_user(); } print "Saving data to $infodir\n"; # Make sure the versions exist check_versions(); # Fetch patches if needed. Get ids of first and last commits my @commits = `git log --pretty=%H "$old_version..$new_version" 2>/dev/null`; get_commits(@commits); my $last_commit_id = $commits[0]; my $first_commit_id = $commits[ @commits - 1 ]; chomp $last_commit_id; chomp $first_commit_id; print "Statistics from commit $first_commit_id to commit $last_commit_id\n"; print "Patch, Date, Owner, Author, Submitter, Inserted lines, Deleted lines, Subject, Reviewers, Commenters\n"; # Loop through all commits for my $commit_id (@commits) { $commit_id =~ s/^\s+|\s+$//g; my $submitter = ""; my %patch_reviewers = (); my %patch_commenters = (); my $info; my $owner; my $author; my $author_email; my $inserted_lines = 0; my $deleted_lines = 0; my $subject; $number_of_commits++; print "\"$commit_id\", "; # Read the data file for the current commit if ( -f "$infodir/$commit_id" && -s "$infodir/$commit_id" > 20 ) { open( my $HANDLE, "<", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n"; $info = <$HANDLE>; close $HANDLE; my $commit_info = JSON::Util->decode($info); # Get the easy data $owner = $commit_info->{'owner'}{'name'}; if ( !$owner ) { $owner = $commit_info->{'owner'}{'username'}; } if ( !$owner ) { $owner = "-"; } if ( $owner && exists( $aliases{$owner} ) ) { $owner = $aliases{$owner}; } $owner =~ s/"/'/g; $owner =~ s/,/./g; $author = $commit_info->{'currentPatchSet'}{'author'}{'name'}; if ( $author && exists( $aliases{$author} ) ) { $author = $aliases{$author}; } if ( !$author ) { $author = $commit_info->{'currentPatchSet'}{'author'}{'username'}; } $author =~ s/"/'/g; $author =~ s/,/./g; $author_email = $commit_info->{'currentPatchSet'}{'author'}{'email'}; $inserted_lines = $commit_info->{'currentPatchSet'}{'sizeInsertions'}; $deleted_lines = $commit_info->{'currentPatchSet'}{'sizeDeletions'}; $subject = $commit_info->{'subject'}; $subject =~ s/"/'/g; #get the patch's submitter my $approvals = $commit_info->{'currentPatchSet'}{'approvals'}; for my $approval (@$approvals) { if ( $approval->{'type'} eq "SUBM" ) { $submit_epoch = $approval->{'grantedOn'}; $submitter = $approval->{'by'}{'name'}; if ( exists( $aliases{$submitter} ) ) { $submitter = $aliases{$submitter}; } } } # Get all the commenters for all patch revisions my $comments = $commit_info->{'comments'}; for my $comment (@$comments) { my $commenter; if ( $comment->{'reviewer'}{'username'} ) { if ( $comment->{'reviewer'}{'username'} eq "jenkins" ) { next; } if ( $comment->{'reviewer'}{'username'} eq "hardwaretestrobot" ) { next; } if ( $comment->{'reviewer'}{'username'} eq "raptor-automated-test" ) { next; } } if ( $comment->{'reviewer'}{'name'} ) { if ( $comment->{'reviewer'}{'name'} eq "Gerrit Code Review" ) { next; } } if ( $comment->{'message'} ) { if ( $comment->{'message'} =~ "successfully cherry-picked" ) { next; } if ( $comment->{'message'} =~ ": Code-Review" ) { next; } if ( $comment->{'message'} =~ "Uploaded patch set" ) { next; } } if ( !$commenter ) { $commenter = $comment->{'reviewer'}{'name'}; if ( $commenter && exists( $aliases{$commenter} ) ) { $commenter = $aliases{$commenter}; } } if ( !$commenter ) { $commenter = $comment->{'reviewer'}{'username'}; if ( $commenter && exists( $aliases{$commenter} ) ) { $commenter = $aliases{$commenter}; } } $commenter =~ s/"/'/g; $commenter =~ s/,/./g; if ( $commenter && $author && $commenter eq $author ) { next; } if ($commenter) { if ( $commenter && exists $patch_commenters{$commenter} ) { $patch_commenters{$commenter}++; } else { $patch_commenters{$commenter} = 1; } } } # Get all the reviewers for all patch revisions my $patchsets = $commit_info->{'patchSets'}; for my $patch (@$patchsets) { if ( !$author ) { $author = $patch->{'author'}{'name'}; if ( $author && exists( $aliases{$author} ) ) { $author = $aliases{$author}; } } my $approvals = $patch->{'approvals'}; for my $approval (@$approvals) { if ( ( !$submitter ) && ( $approval->{'type'} eq "SUBM" ) ) { $submit_epoch = $approval->{'grantedOn'}; $submitter = $approval->{'by'}{'name'}; if ( $submitter && exists( $aliases{$submitter} ) ) { $submitter = $aliases{$submitter}; } } $submitter =~ s/"/'/g; $submitter =~ s/,/./g; if ( $approval->{'type'} eq "Code-Review" ) { my $patch_reviewer = $approval->{'by'}{'name'}; if ($patch_reviewer) { if ( exists $patch_reviewers{$patch_reviewer} ) { $patch_reviewers{$patch_reviewer}++; } else { $patch_reviewers{$patch_reviewer} = 1; } } } } } } else { # Get the info from git my $logline = `git log --pretty="%ct@@@%s@@@%an@@@%aE@@@%cn" $commit_id^..$commit_id --`; $logline =~ m/^(.*)@@@(.*)@@@(.*)@@@(.*)@@@(.*)\n/; ( $submit_epoch, $subject, $author, $author_email, $submitter ) = ( $1, $2, $3, $4, $5 ); if ( exists( $aliases{$author} ) ) { $author = $aliases{$author}; } $owner = $author; if ( $submitter && exists( $aliases{$submitter} ) ) { $submitter = $aliases{$submitter}; } $logline = `git log --pretty= --shortstat $commit_id^..$commit_id --`; if ( $logline =~ m/\s+(\d+)\s+insertion/ ) { $inserted_lines = $1; } if ( $logline =~ m/\s+(\d+)\s+deletion/ ) { $deleted_lines = $1 * -1; } my @loglines = `git log $commit_id^..$commit_id -- | grep '\\sReviewed-by:'`; for my $line (@loglines) { if ( $line =~ m/.*:\s+(.*)\sfrom_epoch( epoch => $submit_epoch ); $dt->set_time_zone('Europe/Paris'); my $submit_time = $dt->strftime('%Y/%m/%d'); if ( !$first_submit_epoch ) { $first_submit_epoch = $submit_epoch; } # Create the list of commenters to print my $commenterlist = ""; foreach my $commenter ( keys %patch_commenters ) { if ( $commenter && exists( $aliases{$commenter} ) ) { $commenter = $aliases{$commenter}; } if ( $commenterlist eq "" ) { $commenterlist = $commenter; } else { $commenterlist .= ", $commenter"; } if ( $commenter && exists $commenters{$commenter} ) { $commenters{$commenter}++; } else { $commenters{$commenter} = 1; } } if ( !$commenterlist ) { $commenterlist = "-"; } # Create the list of reviewers to print my $reviewerlist = ""; foreach my $reviewer ( keys %patch_reviewers ) { if ( exists( $aliases{$reviewer} ) ) { $reviewer = $aliases{$reviewer}; } if ( $reviewerlist eq "" ) { $reviewerlist = $reviewer; } else { $reviewerlist .= ", $reviewer"; } if ( $reviewer && exists $reviewers{$reviewer} ) { $reviewers{$reviewer}++; } else { $reviewers{$reviewer} = 1; } } if ( !$reviewerlist ) { $reviewerlist = "-"; } if ($print_commit_list) { print "$submit_time, $owner, $author, $submitter, $inserted_lines, $deleted_lines, \"$subject\", \"$reviewerlist\" , \"$commenterlist\"\n"; } else { print "$number_of_commits\n"; } $total_added += $inserted_lines; if ( $inserted_lines - $deleted_lines > 100 ) { $patches_over_100_lines++; $total_lines_large_patches += $inserted_lines; } $total_removed += $deleted_lines; if ( exists $owners{$owner} ) { $owners{$owner}++; } else { $owners{$owner} = 1; } if ( $author && exists $authors{$author}{"num"} ) { $authors{$author}{"num"}++; $author_added{$author} += $inserted_lines; $author_removed{$author} += $deleted_lines; $authors{$author}{"earliest_commit"} = $submit_time; } else { $authors{$author}{"num"} = 1; $authors{$author}{"latest_commit"} = $submit_time; $authors{$author}{"earliest_commit"} = $submit_time; $author_added{$author} = $inserted_lines; $author_removed{$author} = $deleted_lines; } if ( $author && ( !exists $authors{$author}{email} || $authors{$author}{email} eq "-" ) ) { if ($author_email) { $authors{$author}{email} = "$author_email"; } elsif ( exists $email_addresses{$author} ) { $authors{$author}{email} = $email_addresses{$author}; } } } my $Days = ( $first_submit_epoch - $submit_epoch ) / 86400; if ( ( $first_submit_epoch - $submit_epoch ) % 86400 ) { $Days += 1; } print "\n* Total Commits: $number_of_commits\n"; printf "* Average Commits per day: %.2f\n", $number_of_commits / $Days; print "* Total lines added: $total_added\n"; printf "* Average lines added per commit: %.2f\n", $total_added / $number_of_commits; print "* Number of patches adding more than 100 lines: $patches_over_100_lines\n"; printf "* Average lines added per small commit: %.2f\n", ( $total_added - $total_lines_large_patches ) / ( $number_of_commits - $patches_over_100_lines ); print "* Total lines removed: $total_removed\n"; printf "* Average lines removed per commit: %.2f\n", $total_removed / $number_of_commits; print "* Total difference between added and removed: " . ( $total_added - $total_removed ) . "\n\n"; print "=== Authors - Number of commits ===\n"; printf "%-30s ,%5s ,%5s ,%6s ,%6s , %-52s ,%6s, %-19s , %s\n", "Author", "Ptchs", "Revws", "Cmnts", "Sbmts", "Email", "Prcnt", "Last commit", "Earliest_commit"; my $number_of_authors = 0; foreach my $author ( sort { $authors{$b}{num} <=> $authors{$a}{num} } ( keys %authors ) ) { my $submissions = 0; if ( $author && exists $submitters{$author} ) { $submissions = $submitters{$author}{count}; } my $review_count = 0; if ( $author && exists $reviewers{$author} ) { $review_count = $reviewers{$author}; } my $comment_count = 0; if ( $author && exists $commenters{$author} ) { $comment_count = $commenters{$author}; } if ( $author && !exists $authors{$author}{"email"} ) { $authors{$author}{"email"} = "-"; } printf "%-30s ,%5d ,%5d ,%6d ,%6d , %-52s ,%5.2f%%, %s , %s\n", $author, $authors{$author}{"num"}, $review_count, $comment_count, $submissions, $authors{$author}{"email"}, $authors{$author}{"num"} / $number_of_commits * 100, $authors{$author}{"latest_commit"}, $authors{$author}{"earliest_commit"}; $number_of_authors++; } print "Total Authors: $number_of_authors\n\n"; print "=== Authors - Lines added ===\n"; foreach my $author ( sort { $author_added{$b} <=> $author_added{$a} } ( keys %author_added ) ) { if ( $author_added{$author} ) { printf "%-30s, %10d, %2.3f%%\n", $author, $author_added{$author}, $author_added{$author} / $total_added * 100; } } print "\n"; print "=== Authors - Lines removed ===\n"; foreach my $author ( sort { $author_removed{$b} <=> $author_removed{$a} } ( keys %author_removed ) ) { if ( $author_removed{$author} ) { printf "%-30s, %10d, %6.3f%%\n", $author, $author_removed{$author} * -1, $author_removed{$author} / $total_removed * 100; } } print "\n"; print "=== Reviewers - Number of patches reviewed ===\n"; my $number_of_reviewers = 0; foreach my $reviewer ( sort { $reviewers{$b} <=> $reviewers{$a} } ( keys %reviewers ) ) { printf "%-30s, %6d, %6.3f%%\n", $reviewer, $reviewers{$reviewer}, $reviewers{$reviewer} / $number_of_commits * 100; $number_of_reviewers++; } print "Total Reviewers: $number_of_reviewers\n\n"; print "=== Submitters - Number of patches submitted ===\n"; printf "%-30s, %6s, %7s, %6s, %7s, %6s, %7s\n", "Name", "#", "total%", "Own", "own%", "Other", "other%"; foreach my $submitter ( sort { $submitters{$b}{count} <=> $submitters{$a}{count} } ( keys %submitters ) ) { printf "%-30s, % 6d, %6.3f%%, %6d, %6.2f%%, %6d, %6.2f%%\n", $submitter, $submitters{$submitter}{count}, $submitters{$submitter}{count} / $number_of_commits * 100, $submitters{$submitter}{"self"}, $submitters{$submitter}{"self"} / $submitters{$submitter}{count} * 100, $submitters{$submitter}{others}, $submitters{$submitter}{others} / $submitters{$submitter}{count} * 100; } print "Total Submitters: $number_of_submitters\n\n"; print "Commits, Ave, Added, Removed, Diff, Authors, Reviewers, Submitters\n"; printf "$number_of_commits, %.2f, $total_added, $total_removed, " . ( $total_added + $total_removed ) . ", $number_of_authors, $number_of_reviewers, $number_of_submitters\n", $number_of_commits / $Days; } #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- sub check_versions { `git cat-file -e $old_version^{commit} 2>/dev/null`; if ( ${^CHILD_ERROR_NATIVE} ) { print "Error: Old version ($old_version) does not exist.\n"; exit 1; } `git cat-file -e $new_version^{commit} 2>/dev/null`; if ( ${^CHILD_ERROR_NATIVE} ) { print "Error: New version ($new_version) does not exist.\n"; exit 1; } } #------------------------------------------------------------------------------- sub get_user { my $url = `git config -l | grep remote.origin.url`; if ( $url =~ /.*url=ssh:\/\/(\w+@[a-zA-Z][a-zA-Z0-9\.]+:\d+)(\/\w+)*/ ) { $URL_WITH_USER = $1; } else { print "Error: Could not get a ssh url with a username from gitconfig.\n"; print " use the -u option to set a url.\n"; exit 1; } } #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- sub get_commits { my @commits = @_; my $submit_time = ""; if ( defined $SKIP_GERRIT_CHECK && $SKIP_GERRIT_CHECK ) { return; } my $ssh = Net::OpenSSH->new( "$URL_WITH_USER", ); $ssh->error and die "Couldn't establish SSH connection to $URL_WITH_USER:" . $ssh->error; print "Using URL: ssh://$URL_WITH_USER\n"; if ( !-d $infodir ) { mkpath($infodir); } for my $commit_id (@commits) { $commit_id =~ s/^\s+|\s+$//g; $submit_time = ""; my $gerrit_review; # Look for last coreboot commit if ( $commit_id eq "7309709742" ) { last; } if ( -f "$infodir/$commit_id" ) { $gerrit_review = 1; } else { $gerrit_review = `git log $commit_id^..$commit_id | grep '\\sReviewed-on:\\s'`; } if ( $gerrit_review && $commit_id && ( !-f "$infodir/$commit_id" ) ) { print "Downloading $commit_id"; my @info = $ssh->capture("gerrit query --format=JSON --comments --files --current-patch-set --all-approvals --submit-records --dependencies commit:$commit_id"); $ssh->error and die "remote ls command failed: " . $ssh->error; my $commit_info = JSON::Util->decode( $info[0] ); my $rowcount = $commit_info->{'rowCount'}; if ( defined $rowcount && ( $rowcount eq "0" ) ) { print " - no gerrit commit for that id.\n"; open( my $HANDLE, ">", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n"; print $HANDLE "No gerrit commit"; close $HANDLE; next; } my $approvals = $commit_info->{'currentPatchSet'}{'approvals'}; for my $approval (@$approvals) { if ( $approval->{'type'} eq "SUBM" ) { $submit_time = $approval->{'grantedOn'}; } } my $dt = ""; if ($submit_time) { $dt = DateTime->from_epoch( epoch => $submit_time ); } else { print " - no submit time for that id.\n"; open( my $HANDLE, ">", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n"; print $HANDLE "No submit time"; close $HANDLE; next; } open( my $HANDLE, ">", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n"; print $HANDLE $info[0]; close $HANDLE; $dt->set_time_zone('Europe/Paris'); print " - submit time: " . $dt->strftime('%Y/%m/%d %H:%M:%S') . "\n"; } elsif ( $commit_id && ( !-f "$infodir/$commit_id" ) ) { print "No gerrit commit for $commit_id\n"; open( my $HANDLE, ">", "$infodir/$commit_id" ) or die "Error: could not open file '$infodir/$commit_id'\n"; print $HANDLE "No gerrit commit"; close $HANDLE; } } print "\n"; } #------------------------------------------------------------------------------- # check_arguments parse the command line arguments #------------------------------------------------------------------------------- sub check_arguments { my $show_usage = 0; GetOptions( 'help|?' => sub { usage() }, 'url|u=s' => \$URL_WITH_USER, 'skip|s' => \$SKIP_GERRIT_CHECK, ); # strip ssh:// from url if passed in. if ( defined $URL_WITH_USER ) { $URL_WITH_USER =~ s|ssh://||; } if (@ARGV) { ( $old_version, $new_version ) = @ARGV; } else { usage(); } } #------------------------------------------------------------------------------- # usage - Print the arguments for the user #------------------------------------------------------------------------------- sub usage { print "gerrit_stats [Old version] [New version]\n"; print "Old version should be a tag (4.1), a branch (origin/4.1), or a commit id\n"; print "New version can be 'HEAD' a branch (origin/main) a tag (4.2), or a commit id\n"; print " Options:\n"; print " u | url [url] url with username.\n"; print "Example: \"$0 -u Gaumless\@review.coreboot.org:29418 origin/4.1 4.2\"\n"; exit(0); } 1;