#!/usr/bin/perl
our
$VERSION
=
'0.60'
;
branch_basis
branch_point
confirm
current_branch
fetch_tags
full_changeset_name
guarantee_a_clean_working_directory
get_user_name
get_user_email
git
git_config
git_fetch_and_clean_up
is_auto_fetch
is_valid_ref
its_for_changeset
let_user_edit
meta_data_add
meta_data_rm
project_config
project_name
user_lookup_class
)
;
my
$self_review
= 0;
my
$keep
= 0;
my
$skip_email
= 0;
my
$skip_edit
= 0;
my
$auto_rebase
= 1;
GetOptions(
'keep'
=> \
$keep
,
'skip-email'
=> \
$skip_email
,
'skip-edit'
=> \
$skip_edit
,
'skip-auto-rebase'
=>
sub
{
$auto_rebase
= 0 },
);
my
$reviewer
= find_reviewer(
shift
@ARGV
);
my
$changeset
= current_branch();
die
"You may not submit $changeset for review.\n"
.
"Perhaps you should checkout a changeset branch first.\n"
if
$changeset
=~ m/^(master|test|stage|prod)$/;
git_fetch_and_clean_up()
if
is_auto_fetch();
my
$branch_point
= branch_point( full_changeset_name(
$changeset
) );
my
@commit_ids
= git
"rev-list HEAD ^$branch_point"
;
die
"You haven't committed anything to this changeset yet!\n"
if
not
@commit_ids
;
launch_auto_rebase(
$branch_point
)
if
$auto_rebase
;
my
$its
= its_for_changeset(
$changeset
);
my
$stash
;
reversibly {
failure_warning
"\nCanceling gitc submit\n"
;
$stash
= guarantee_a_clean_working_directory();
to_undo { git
"stash apply $stash"
if
$stash
;
$stash
=
undef
};
if
(
my
@conflicts
= find_merge_conflicts(
$changeset
) ) {
my
$conflicts
=
join
', '
,
@conflicts
;
warn
"\nThis changeset will have merge conflicts when promoted\n"
.
"to $conflicts. In your submit email, please provide the\n"
.
"reviewer with any instructions he might need to correctly\n"
.
"resolve the conflicts.\n"
.
"Press ENTER to continue.\n"
;
;
my
$junk
= <STDIN>;
}
my
(
$tmpdir
,
$cover_letter
) = export_patches(
$changeset
);
let_user_edit(
$cover_letter
)
unless
(
$skip_email
||
$skip_edit
);
die
"Aborting submit at the user's request\n"
if
-s
$cover_letter
<= 10;
update_email_headers(
$tmpdir
,
$cover_letter
);
my
$id
= meta_data_add({
action
=>
'submit'
,
changeset
=>
$changeset
,
reviewer
=>
$reviewer
,
});
to_undo { meta_data_rm(
id
=>
$id
,
changeset
=>
$changeset
) };
if
(
$self_review
) {
my
$id
= meta_data_add({
action
=>
'review'
,
changeset
=>
$changeset
,
});
to_undo { meta_data_rm(
id
=>
$id
,
changeset
=>
$changeset
) };
}
if
( not
$self_review
) {
git
"push --force origin $changeset:pu/$changeset"
;
to_undo { git
"push origin :pu/$changeset"
};
}
if
( not
$keep
) {
git
"checkout master"
;
to_undo { git
"checkout -f $changeset"
};
local
$@;
eval
{
my
$sha1
= is_valid_ref(
$changeset
);
git
"branch -D $changeset"
;
to_undo { git
"branch $changeset $sha1"
};
};
warn
"Unable to delete branch $changeset: $@\n"
if
$@;
}
git
"send-email --to "
. get_user_email(
$reviewer
)
.
' --from "'
. author_email() .
'"'
.
' --no-chain-reply-to'
.
' --signed-off-by-cc'
.
' --suppress-cc author'
.
' --suppress-from'
.
' --quiet'
.
' --no-validate'
.
" $tmpdir/*.patch"
if
not
$skip_email
;
if
(
$its
) {
my
$issue
=
$its
->get_issue(
$changeset
,
reload
=> 1);
my
$project
= project_name();
my
$what_happened
=
$its
->transition_state({
command
=>
'submit'
,
issue
=>
$issue
,
reviewer
=>
$reviewer
,
message
=>
"Submitted $project#$changeset to $reviewer for code review"
,
changeset
=>
$changeset
,
});
}
return
;
};
git
"stash apply $stash"
if
$stash
;
exec
"gitc pass --from-self-review"
if
$self_review
;
exit
;
sub
find_reviewer {
my
$reviewer
=
shift
or
die
"You must specify a reviewer\n"
;
my
$current_user
= get_user_name();
if
(
$reviewer
eq
$current_user
) {
if
( project_config()->{
'self submit'
} ) {
$self_review
= 1;
$skip_email
= 1;
$keep
= 1;
return
$reviewer
;
}
}
validate_reviewer(
$reviewer
);
return
$reviewer
;
}
sub
validate_reviewer {
my
(
$reviewer
) =
@_
;
fetch_tags();
my
@users
= user_lookup_class()->users();
return
if
any {
$_
eq
$reviewer
}
@users
;
my
@suggestions
=
map
{
$_
->[0] }
sort
{
$a
->[1] <=>
$b
->[1] }
grep
{
$_
->[1] < 4 }
map
{ [
$_
,
scalar
Text::Levenshtein::distance(
$reviewer
,
$_
) ] }
@users
;
my
@short_list
=
@suggestions
[ 0 .. List::Util::min(
$#suggestions
, 2) ];
my
$msg
=
"The user name '$reviewer' is invalid. "
;
if
(
@suggestions
) {
$msg
.=
"Perhaps you meant one of:\n"
;
$msg
.=
" - $_\n"
for
@short_list
;
}
else
{
$msg
.=
"\n"
;
}
die
$msg
;
}
sub
launch_auto_rebase {
my
(
$branch_point
) =
@_
;
my
$basis
= branch_basis(
$branch_point
);
return
if
$basis
!~ /^(master|test|stage|prod)$/;
my
@upstream
= git
"rev-list --first-parent origin/$basis ^HEAD"
;
return
if
not
@upstream
;
my
$count
=
@upstream
;
my
$s
=
$count
== 1 ?
''
:
's'
;
warn
"Uh oh, $basis has $count commit$s since you started.\n"
.
"I'm rebasing for you. When it's done, resubmit.\n"
.
"\n"
;
exec
"git rebase --onto origin/$basis $branch_point"
;
}
sub
export_patches {
my
(
$changeset
) =
@_
;
my
$tmpdir
= File::Temp::tempdir(
'gitc-submit-XXXXX'
,
TMPDIR
=> 1,
CLEANUP
=> 1,
);
my
$project
= project_name();
git
"format-patch -o $tmpdir"
.
" --thread"
.
" --no-numbered"
.
" --cover-letter"
.
" --no-color"
.
" --no-binary"
.
" -M -C --no-ext-diff"
.
" --no-prefix"
.
" --subject-prefix='$project#$changeset'"
.
" "
. branch_point(
$changeset
)
;
my
@patches
=
glob
(
"$tmpdir/*.patch"
);
if
(
@patches
> 2 and
$its
) {
fill_in_subject_line(
$tmpdir
,
$changeset
);
return
(
$tmpdir
,
"$tmpdir/0000-cover-letter.patch"
);
}
unlink
"$tmpdir/0000-cover-letter.patch"
;
my
(
$patch
) =
glob
(
"$tmpdir/*.patch"
);
if
(
$its
) {
my
$uri
=
$its
->issue_changeset_uri(
$its
->get_issue(
$changeset
) );
if
(
$uri
) {
my
$content
=
do
{
open
my
$fh
,
'<'
,
$patch
or
die
"Couldn't open $patch: $!"
;
local
$/;
<
$fh
>;
};
$content
=~ s{\n\n}{\n\n
$uri
\n\n};
open
my
$fh
,
'>'
,
$patch
or
die
"Couldn't write to $patch: $!"
;
print
$fh
$content
;
}
}
return
(
$tmpdir
,
$patch
);
}
sub
fill_in_subject_line {
my
(
$tmpdir
,
$changeset
) =
@_
;
my
$file
=
"$tmpdir/0000-cover-letter.patch"
;
open
my
$fh
,
'<'
,
$file
or
die
"Couldn't read cover letter : $?"
;
my
$content
=
do
{
local
$/; <
$fh
> };
close
$fh
;
my
$its_name
=
$its
->label_service;
my
$its_label
=
$its
->label_issue;
my
$issue
=
$its
->get_issue(
$changeset
);
my
$subject
=
eval
{
print
STDERR
"Looking for $its_name $its_label..."
;
my
$summary
=
$its
->issue_summary(
$issue
);
print
STDERR
"done\n"
;
return
$summary
;
} ||
"Submitted for Review"
;
warn
"Problem obtaining the $its_label summary: $@"
if
$issue
and $@;
$content
=~ s/\Q*** SUBJECT HERE ***\E/
$subject
/;
my
$uri
=
$its
->issue_changeset_uri(
$issue
);
$content
=~ s/\Q*** BLURB HERE ***\E/
$uri
/;
open
$fh
,
'>'
,
$file
or
die
"Couldn't write cover letter : $?"
;
print
$fh
$content
;
close
$fh
;
return
;
}
sub
update_email_headers {
my
(
$tmpdir
,
$cover_letter
) =
@_
;
return
if
$cover_letter
!~ m/0000-cover-letter/;
my
@blacklist
=
qw(
date
from
in-reply-to
message-id
references
subject
)
;
my
$cover
= Email::Simple->new( slurp(
$cover_letter
) );
my
%extra_headers
;
for
my
$header
(
$cover
->header_names ) {
next
if
$header
=~ m/^from /i;
$extra_headers
{
lc
$header
} =
$cover
->header(
$header
);
}
delete
@extra_headers
{
@blacklist
};
for
my
$file
(
glob
"$tmpdir/*.patch"
) {
next
if
$file
=~ m/0000-cover-letter/;
my
$email
= Email::Simple->new( slurp(
$file
) );
while
(
my
(
$name
,
$value
) =
each
%extra_headers
) {
$email
->header_set(
$name
,
$value
);
}
open
my
$fh
,
'>'
,
$file
or
die
"Unable to write to $file: $!"
;
print
$fh
$email
->as_string;
}
return
;
}
sub
author_email {
my
$name
= get_user_name();
my
$email
= get_user_email();
return
"$name <$email>"
;
}
sub
find_merge_conflicts {
my
(
$changeset
) =
@_
;
warn
"Looking for merge conflicts...\n"
;
git
"checkout -q --no-track -b test-merges origin/master"
;
my
@conflicts
;
for
my
$environment
(
qw( master )
) {
git
"reset -q --hard origin/$environment"
;
my
$output
= git
"merge --quiet --no-stat --no-ff $changeset"
;
push
@conflicts
,
$environment
if
$output
=~ m/Automatic merge failed/;
}
git
"reset --hard"
;
git
"checkout -q -f $changeset"
;
my
$output
= git
"branch -D test-merges"
;
return
@conflicts
;
}
sub
slurp {
my
(
$filename
) =
@_
;
open
my
$fh
,
'<'
,
$filename
or
die
"Unable to open $filename: $!"
;
my
$content
=
do
{
local
$/; <
$fh
> };
return
$content
;
}