our
$VERSION
=
'0.60'
;
use
constant
GITC_CONFIG
=>
'/etc/gitc/gitc.config'
;
BEGIN {
our
@EXPORT
=
qw(
current_branch
eventum
eventum_transition_status
fetch_tags
get_user_name
get_user_email
git
git_config
guarantee_a_clean_working_directory
let_user_edit
meta_data_add
meta_data_rm
meta_data_rm_all
project_config
its
its_for_changeset
)
;
our
@EXPORT_OK
=
qw(
add_current_user
archived_tags
branch_basis
branch_point
cache_meta_data
changeset_group
changeset_merged_to
changesets_in
changesets_promoted_between
command_name
commit_decorations
confirm
current_branch_version
environment_preceding
full_changeset_name
git_fetch_and_clean_up
git_dir
git_tag
highest_quickfix_number
history
history_owner
history_reviewer
history_status
history_submitter
is_auto_fetch
is_merge_commit
is_suspendable
is_valid_ref
meta_data_rm_project
new_branch_version
new_version_tag
open_packed_refs
parse_changeset_spec
project_name
project_root
remote_branch_exists
restore_meta_data
sendmail
short_ref_name
sort_changesets_by_name
toplevel
traverse_commits
unmerged_changesets
unpromoted
user_lookup_class
version_tag_prefix
state_blocked
)
;
}
sub
confirm {
my
(
$message
) =
@_
;
die
"No message given to 'confirm'"
if
not
defined
$message
;
my
$term
= Term::ReadLine->new(
'gitc'
);
my
$prompt
=
"$message "
;
my
$response
;
while
(
defined
(
$response
=
$term
->
readline
(
$prompt
) ) ) {
return
1
if
$response
eq
'y'
;
return
if
$response
eq
'n'
;
$prompt
=
"$message ('y' or 'n') "
;
}
}
sub
current_branch {
my
(
$name
) =
grep
/^[*]/,
qx{ git branch --no-color }
;
chomp
$name
;
$name
=~ s/^[*] //;
return
$name
;
}
sub
its_config {
my
$name
=
lc
its()->label_service;
return
project_config()->{
"${name}_statuses"
};
}
sub
eventum {
return
its()->get_issue(
@_
);
}
sub
eventum_transition_status {
return
its()->transition_state(
@_
);
}
sub
eventum_statuses {
my
(
$self
,
$command
,
$target
) =
@_
;
my
$statuses
= project_config()->{
'jira_statuses'
}{
$command
}
or
die
"No JIRA statuses for $command"
;
if
( not
$target
) {
die
"No initial status"
unless
$statuses
->{from};
die
"No final status"
unless
$statuses
->{to};
return
(
$statuses
);
}
die
"No initial status for target $target"
unless
$statuses
->{
$target
}{from};
die
"No final status for target $target"
unless
$statuses
->{
$target
}{to};
return
$statuses
->{
$target
};
}
sub
_package_its {
my
$its_type
=
shift
;
return
'App::Gitc::Its::'
.
ucfirst
$its_type
;
}
sub
its {
my
$its
=
shift
|| project_config()->{
'default_its'
};
return
undef
unless
$its
;
Class::MOP::load_class(_package_its(
$its
))
or
die
"I can't load $its: $!.\n"
;
return
_package_its(
$its
)
}
sub
user_lookup_class {
my
$lookup_class
= project_config()->{ user_lookup_method }
//
'LocalGroup'
;
my
$pkg
=
"App::Gitc::UserLookup::$lookup_class"
;
Class::MOP::load_class(
$pkg
);
return
$pkg
;
}
sub
its_for_changeset {
my
(
$changeset
) =
@_
;
my
$its
= its();
return
$its
->can(
'its_for_changeset'
)
?
$its
->its_for_changeset(
$changeset
)
:
$its
;
}
sub
git {
my
(
$command_line
) =
@_
;
my
$start
= Cwd::cwd();
my
$base
= (
$command_line
=~ /^clone / ?
$start
: toplevel() );
unless
(
$start
eq
$base
) {
chdir
$base
||
warn
"Could not cd to $base"
;
}
if
( not
defined
wantarray
) {
warn
"> git $command_line\n"
if
$ENV
{DEBUG};
system
(
"git $command_line"
) == 0 and
return
;
my
$msg
=
''
;
if
( $? == -1 ) {
$msg
=
"failed to execute: $!"
;
}
elsif
( $? & 127 ) {
$msg
=
sprintf
"died with signal %d"
, ( $? & 127 );
}
else
{
$msg
=
sprintf
"exited with value %d"
, ( $? >> 8 );
}
Carp::croak(
"git $command_line failed: $msg"
);
}
elsif
(
wantarray
) {
warn
"> git $command_line\n"
if
$ENV
{DEBUG};
my
@output
=
qx{git $command_line}
;
chomp
@output
;
return
@output
;
}
warn
"> git $command_line\n"
if
$ENV
{DEBUG};
my
$output
=
qx{git $command_line}
;
if
( not
defined
$output
) {
Carp::croak(
"git $command_line failed: $!"
);
}
chdir
$start
unless
$start
eq
$base
;
chomp
$output
;
return
$output
;
}
sub
git_config {
our
%config
;
if
( not
keys
%config
) {
for
my
$line
( git
"config -l"
) {
my
(
$name
,
$value
) =
split
/=/,
$line
;
my
@parts
=
split
/[.]/,
$name
;
my
$here
= \
%config
;
for
my
$part
(
@parts
[ 0 ..
$#parts
-1 ] ) {
$here
->{
$part
} = {}
if
not
$here
->{
$part
};
$here
=
$here
->{
$part
};
}
$here
->{
$parts
[-1] } =
$value
;
}
}
return
\
%config
;
}
sub
guarantee_a_clean_working_directory {
my
$arguments
=
"diff -C -M --name-status"
;
my
$staged
= git
"$arguments --cached"
;
my
$changed
= git
$arguments
;
return
if
not
$staged
and not
$changed
;
warn
"It looks like you have uncommitted changes. If this is expected,\n"
.
"type 'y' to continue. If it's not expected, type 'n'.\n"
. (
$staged
?
"staged:\n$staged\n"
:
''
)
. (
$changed
?
"changed:\n$changed\n"
:
''
)
;
die
"Aborting at the user's request.\n"
if
not confirm('Continue?');
my
$stash
= git
"stash create"
;
git
"reset --hard"
;
return
$stash
;
}
sub
let_user_edit {
my
(
$filename
) =
@_
;
my
$editor
=
$ENV
{EDITOR} ||
$ENV
{VISUAL} ||
'/usr/bin/vim'
;
system
"$editor $filename"
;
}
sub
create_blob {
my
(
$data_ref
) =
@_
;
my
$tmp_file
=
"meta-$$.tmp"
;
open
my
$tmp
,
">"
,
$tmp_file
;
print
{
$tmp
} Dump(
$data_ref
);
print
{
$tmp
}
"\n"
;
my
$blob
= git
"hash-object -w $tmp_file"
;
close
$tmp
;
unlink
$tmp_file
;
return
$blob
;
}
sub
view_blob {
my
(
$ref
) =
@_
;
my
$output
= git
"show $ref"
;
return
(
$output
and
$output
!~ /^fatal:/) ? Load(
$output
) :
undef
;
}
sub
get_user_name {
my
$git_user
= git
'config --get user.name'
;
my
$git_config
= git_config();
return
$git_user
||
$git_config
->{user}{name} ||
getpwuid
$>;
}
sub
get_user_email {
my
(
$user
) =
@_
;
return
git
'config --get user.email'
unless
$user
;
fetch_tags();
my
$git_config
= git_config();
my
$user_info
= view_blob(
"user/$user"
) || {};
return
$user_info
->{email} ||
$git_config
->{user}{email} ||
$user
;
}
sub
add_current_user {
my
$user
= get_user_name();
my
$email
= get_user_email();
die
"You need to configure a git username and email."
unless
$user
ne
$email
;
my
$user_info
= {
email
=>
$email
};
my
$blob
= create_blob(
$user_info
);
git_tag(
'-d'
,
"user/$user"
)
if
view_blob(
"user/$user"
);
git_tag(
"user/$user"
,
$blob
);
return
git
"push --force origin user/$user"
;
}
sub
meta_data_add {
my
(
$entries
) =
@_
;
if
(
ref
(
$entries
) ne
'ARRAY'
) {
$entries
=
$entries
? [
$entries
] : [];
}
my
@meta_tags
= get_meta_tags();
my
%meta_tags
;
++
$meta_tags
{
$_
}
for
@meta_tags
;
our
$tag_buffer
;
initialize_tag_buffer()
unless
$tag_buffer
;
my
@tags
;
my
$single_id
;
my
$flush
= 1;
for
my
$data
(
@$entries
) {
$data
->{user} = get_user_name()
if
not
exists
$data
->{user};
my
$changeset
=
$data
->{changeset};
my
$meta_info
=
$meta_tags
{
"meta/$changeset"
} ? view_blob(
"meta/$changeset"
) : [];
my
$id
=
scalar
@$meta_info
;
$single_id
=
$id
if
@$entries
== 1;
my
$flag
=
delete
$data
->{flush};
$flush
= 0
if
defined
$flag
and
$flag
== 0;
$data
->{stamp} =
time
;
$meta_info
->[
$id
] =
$data
;
my
$blob
= create_blob(
$meta_info
);
my
$exists
=
grep
{m|^meta/
$changeset
|} get_meta_tags();
git_tag(
'-d'
,
"meta/$changeset"
)
if
$exists
;
git_tag(
"meta/$changeset"
,
$blob
);
push
@tags
,
"meta/$changeset"
;
}
push
@{
$tag_buffer
->{meta_add}},
@tags
;
if
(
my
@rm_tags
= @{
$tag_buffer
->{meta_rm}}) {
for
my
$tag
(
@tags
) {
my
$i
= first_index {
$_
eq
$tag
}
@rm_tags
;
next
unless
defined
$i
;
splice
(
@rm_tags
,
$i
, 1);
}
$tag_buffer
->{meta_rm} = \
@rm_tags
}
if
(
$flush
) {
my
@buffered_tags
= @{
$tag_buffer
->{meta_add}};
git
"push --force origin @buffered_tags"
if
@buffered_tags
;
}
return
if
@$entries
> 1;
return
$single_id
;
}
sub
initialize_tag_buffer {
our
$tag_buffer
= {};
$tag_buffer
->{meta_add} = [];
$tag_buffer
->{meta_rm} = [];
return
;
}
sub
meta_data_rm {
my
@args
= (
ref
$_
[0] eq
'HASH'
) ?
@_
:
@_
? {
@_
} : ();
our
$recent_meta_data
;
++
$recent_meta_data
and git
"fetch origin --tags"
if
not
$recent_meta_data
;
our
$tag_buffer
;
initialize_tag_buffer()
unless
$tag_buffer
;
my
@tags
;
my
$flush
= 1;
for
my
$arg
(
@args
) {
my
$meta_info
= view_blob(
"meta/$arg->{changeset}"
);
return
unless
$meta_info
;
splice
(
@$meta_info
,
$arg
->{id}, 1);
my
$blob
= create_blob(
$meta_info
);
git_tag(
'-d'
,
"meta/$arg->{changeset}"
);
git_tag(
"meta/$arg->{changeset}"
,
$blob
);
push
@tags
,
"meta/$arg->{changeset}"
;
$flush
= 0
if
exists
$arg
->{flush} and
$arg
->{flush} == 0;
}
push
@{
$tag_buffer
->{meta_rm}},
@tags
;
if
(
my
@add_tags
= @{
$tag_buffer
->{meta_add}}) {
for
my
$tag
(
@tags
) {
my
$i
= first_index {
$_
eq
$tag
}
@add_tags
;
next
unless
defined
$i
;
splice
(
@add_tags
,
$i
, 1);
}
$tag_buffer
->{meta_rm} = \
@add_tags
;
}
if
(
$flush
) {
my
@buffered_tags
= @{
$tag_buffer
->{meta_rm}};
git
"push --force origin @buffered_tags"
if
@buffered_tags
;
}
return
;
}
sub
meta_data_rm_all {
my
(
$changeset
) =
@_
;
git
"fetch origin --tags"
;
my
$meta_tag
= (
$changeset
=~ m{^meta/}) ?
$changeset
:
"meta/$changeset"
;
git_tag(
'-d'
,
"$meta_tag"
);
git
"push origin :$meta_tag"
;
}
sub
fetch_tags {
our
$recent_tags
;
++
$recent_tags
and git
"fetch origin --tags"
unless
$recent_tags
;
}
sub
get_meta_tags {
my
(
%args
) =
@_
;
$args
{fetch} //= 1;
fetch_tags()
if
$args
{fetch};
my
$meta_tag_string
= git
"tag -l 'meta/*'"
;
return
split
"\n"
,
$meta_tag_string
;
}
sub
meta_data_rm_project {
my
(
$project
) =
@_
;
my
@meta_tags
= get_meta_tags();
meta_data_rm_all(
$_
)
for
@meta_tags
;
return
;
}
sub
new_branch_version {
my
(
$branch
,
$new_major_version
) =
@_
;
my
$latest
= current_branch_version_details(
$branch
);
my
$major
=
$latest
->{major_version};
my
$minor
=
$latest
->{minor_version} + 1;
if
(
$new_major_version
) {
$major
+= 1;
$minor
= 0;
}
return
"$major.$minor"
;
}
sub
new_version_tag {
my
(
$branch
,
$new_major_version
) =
@_
;
my
$tag_prefix
= version_tag_prefix(
$branch
);
my
$version
= new_branch_version(
$branch
,
$new_major_version
);
return
"$tag_prefix$version"
;
}
sub
project_config {
my
$project_name
=
shift
;
my
@files
= (GITC_CONFIG);
my
$project_file
;
unless
(
$project_name
) {
my
$root
= project_root();
$project_file
=
$root
.
'/gitc.config'
;
push
(
@files
,
$project_file
);
}
push
(
@files
,
$ENV
{HOME} .
'/.gitc/gitc.config'
);
$project_name
||= project_name();
if
(
$ENV
{GITC_CONFIG}) {
push
(
@files
,
split
(
':'
,
$ENV
{GITC_CONFIG}));
}
my
$projects
;
local
$YAML::Syck::UseCode
= 1;
foreach
my
$file
(
@files
) {
next
unless
-f
$file
;
my
$data
=
eval
{YAML::Syck::LoadFile(
$file
)};
if
(
$file
eq
$project_file
) {
if
(!
$data
->{
$project_name
} and
keys
%{
$data
}) {
$data
= {
$project_name
=>
$data
};
}
}
$projects
= merge
$projects
,
$data
;
}
my
$project_config
=
$projects
->{
$project_name
} //
$projects
;
die
"No config found!\n"
if
!
keys
%{
$project_config
// {} };
return
$project_config
;
}
sub
archived_tags {
my
$tag_portion
=
shift
;
my
$dbh
= dbh();
my
$project_name
= project_name();
my
$sql
=
q{
SELECT sha1, tag_name
FROM tag_archive
WHERE project = ?
}
;
if
(
$tag_portion
) {
$sql
.=
'AND tag_name LIKE ?'
;
$tag_portion
=
'%'
.
$tag_portion
.
'%'
;
}
$sql
.=
q{
ORDER BY BINARY tag_name
}
;
my
$refs
=
$dbh
->selectall_arrayref(
$sql
,
undef
,
$project_name
,
$tag_portion
|| () );
return
$refs
;
}
sub
branch_basis {
my
$branch_point
=
shift
or
return
'unknown'
;
my
@decorations
= commit_decorations(
$branch_point
);
for
(
@decorations
) {
return
$1
if
m{/to-(master|test|stage|prod)$};
return
$1
if
m{^refs/remotes/origin/(master|test|stage|prod)$};
return
$1
if
m{^refs/tags/cs/(.*)/head$};
return
$1
if
m{^refs/tags/(test|stage|prod)/[\dTZ_-]{20}$};
return
$1
if
m{^refs/remotes/origin/pu/(.*)$};
}
return
'unknown'
;
}
sub
branch_point {
my
$ref
=
shift
;
$ref
= current_branch()
if
not
defined
$ref
;
my
$changeset
= short_ref_name(
$ref
)
or
die
"You can only find branch points for changeset branches\n"
;
my
$ref_ptr
= is_valid_ref(
$ref
)
or
die
"You gave branch_point an invalid ref\n"
;
my
%to
=
map
{ m{/to-(.*)$} ? ( $
1
=> 1 ) : () }
git
"tag -l cs/$changeset/*"
;
my
@excludes
=
map
{
$to
{
$_
} ?
"^cs/$changeset/to-$_~1"
:
"^origin/$_"
}
qw( master test stage prod )
;
my
$saw_a_commit
;
my
$parent
;
my
$done
;
traverse_commits(
"--first-parent $ref @excludes --"
,
sub
{
my
(
$args
) =
@_
;
$saw_a_commit
= 1;
return
if
$done
;
my
@parents
= @{
$args
->{parents} };
if
(
@parents
> 1 ) {
$done
= 1;
$parent
=
$parents
[1];
return
;
}
for
my
$decoration
( commit_decorations(
$args
->{commit}) ) {
if
(
$decoration
=~ m{/cs/(.*)/head$} and $1 ne
$changeset
) {
$done
= 1;
$parent
=
$args
->{commit};
return
;
}
elsif
(
$decoration
=~ m{/pu/(.*)$} and $1 ne
$changeset
) {
$done
= 1;
$parent
=
$args
->{commit};
return
;
}
}
(
$parent
) = @{
$args
->{parents} };
});
return
$ref_ptr
if
not
$saw_a_commit
;
return
if
not
$parent
;
return
$parent
;
}
sub
changeset_group {
my
(
$changeset
) =
@_
;
die
"Cannot determine the changeset group for undef"
if
not
defined
$changeset
;
my
(
$prefix
,
$number
) =
$changeset
=~ m{
^ ([a-zA-Z-]+)
(\d*)
}xms;
my
$project_name
= project_name();
my
@meta_tags
= get_meta_tags();
my
@changesets
=
grep
{
defined
$prefix
? /
$prefix
$number
/ :
$_
eq
$changeset
}
map
{s{^meta/}{}}
@meta_tags
;
my
@open_changesets
=
grep
{
my
$meta_info
= view_blob(
$_
);
$meta_info
->[-1]{action} eq
'open'
}
@changesets
;
my
$changesets
= \
@open_changesets
;
return
$changesets
if
not
defined
$prefix
;
return
$changesets
if
$prefix
ne
'e'
;
my
@peers
=
grep
{ /(\d+)/ and $1 ==
$number
}
@$changesets
;
sort_changesets_by_name(\
@peers
);
return
\
@peers
;
}
sub
changeset_merged_to {
my
(
$changeset
) =
@_
;
my
@merged_to
;
for
my
$env
(
qw( master test stage prod )
) {
push
@merged_to
,
$env
if
is_valid_ref(
"cs/$changeset/to-$env"
);
}
return
wantarray
?
@merged_to
:
join
(
', '
,
@merged_to
);
}
sub
changesets_promoted_between {
my
(
$args
) =
@_
;
my
$project
=
$args
->{project} or
die
"No project\n"
;
my
$target
=
$args
->{target} or
die
"No target\n"
;
my
$start
=
$args
->{start} or
die
"No start time\n"
;
my
$end
=
$args
->{end} or
die
"No end time\n"
;
$start
= str2time(
$start
);
$end
= str2time(
$end
);
my
@meta_tags
= get_meta_tags();
my
$changesets
= [];
for
my
$tag
(
@meta_tags
) {
my
(
$cs_name
) =
$tag
=~ m{^meta/(.*)};
my
$meta_info
= view_blob(
$tag
);
my
(
$ok_target
,
$ok_time
);
for
my
$entry
(
@$meta_info
) {
(
$ok_target
,
$ok_time
) = ();
next
unless
$entry
->{action} eq
'promote'
;
my
$stamp
=
$entry
->{stamp};
++
$ok_target
if
$entry
->{target} eq
$target
;
++
$ok_time
if
(
$stamp
>
$start
and
$stamp
<
$end
);
}
push
@$changesets
,
$cs_name
if
(
$ok_target
and
$ok_time
);
}
return
@$changesets
;
}
sub
commit_decorations {
my
(
$commit
) =
@_
;
our
%decorations
;
our
$decorations_populated_from_disk
;
if
( not
$decorations_populated_from_disk
) {
$decorations_populated_from_disk
= 1;
%decorations
= ();
my
%packed_refs
;
if
( -e
'.git/packed-refs'
) {
open
my
$refs
,
'<'
,
'.git/packed-refs'
or
die
"Unable to open packed refs: $!\n"
;
while
(
my
$line
= <
$refs
> ) {
chomp
$line
;
next
if
$line
=~ m/^
my
(
$commit
,
$ref
) =
split
/ /,
$line
, 2;
next
if
not
$commit
;
next
if
not
$ref
;
$packed_refs
{
$ref
} =
$commit
;
$decorations
{
$commit
}{
$ref
} = 1;
}
}
open
my
$refs
,
'-|'
,
'find .git/refs -type f'
or
die
"Unable to run find: $!\n"
;
while
(
my
$ref
= <
$refs
> ) {
chomp
$ref
;
my
$commit
=
do
{
open
my
$fh
,
'<'
,
"$ref"
;
local
$/;
<
$fh
>;
};
chomp
$commit
;
$ref
=~ s{\.git/refs/}{};
if
(
my
$stale_commit
=
delete
$packed_refs
{
"refs/$ref"
} ) {
delete
$decorations
{
$stale_commit
}{
"refs/$ref"
};
}
$decorations
{
$commit
}{
"refs/$ref"
} = 1;
}
}
if
( not
$decorations
{
$commit
} ) {
return
if
wantarray
;
return
[];
}
my
@ds
=
keys
%{
$decorations
{
$commit
} };
return
wantarray
?
@ds
: \
@ds
;
}
sub
current_branch_version {
my
(
$branch
) =
@_
;
return
current_branch_version_details(
$branch
)->{full_version};
}
sub
current_branch_version_details {
my
(
$branch
) =
@_
;
die
"Project not setup to support version tagging"
unless
project_config()->{use_version_tags};
my
$tag_prefix
= version_tag_prefix(
$branch
);
my
@versions
= git(
"tag -l $tag_prefix*"
);
return
{
major_version
=> 1,
minor_version
=> 0,
full_version
=>
'1.0'
,
}
unless
@versions
;
@versions
=
map
{ s/
$tag_prefix
//;
$_
}
@versions
;
@versions
=
sort
{
$a
<=>
$b
}
@versions
;
my
$major_version
=
$versions
[-1];
$major_version
=~ s/\.\d+$//;
@versions
=
grep
{ /^
$major_version
\./ }
@versions
;
@versions
=
map
{ s/^
$major_version
\.//;
$_
}
@versions
;
@versions
=
sort
{
$a
<=>
$b
}
@versions
;
my
$minor_version
=
pop
@versions
;
return
{
major_version
=>
$major_version
,
minor_version
=>
$minor_version
,
full_version
=>
"$major_version.$minor_version"
,
};
}
sub
environment_preceding {
my
(
$target
) =
@_
;
my
@environments
=
qw( master test stage prod )
;
my
$i
= first_index {
$target
eq
$_
}
@environments
;
die
"Unknown environment name: $target\n"
if
$i
< 0;
return
if
$i
== 0;
return
$environments
[
$i
-1 ];
}
sub
full_changeset_name {
my
(
$name
,
%params
) =
@_
;
die
"'$name' doesn't look like a changeset name\n"
if
$name
=~ m{/};
our
%cache
;
return
$cache
{
$name
}
if
exists
$cache
{
$name
};
my
$full_name
= is_valid_ref(
"cs/$name/head"
) ?
"cs/$name/head"
: is_valid_ref(
"origin/pu/$name"
) ?
"origin/pu/$name"
: is_valid_ref(
$name
) ?
$name
:
undef
;
unless
(
defined
$full_name
) {
return
if
$params
{missing_ok};
die
"Cannot determine a full changeset name for '$name'\n"
;
}
return
$cache
{
$name
} =
$full_name
;
}
sub
git_dir {
our
$git_dir
;
return
$git_dir
if
defined
$git_dir
;
my
(
$raw
) = git
"rev-parse --git-dir"
;
return
$git_dir
= Cwd::realpath(
$raw
);
}
sub
git_fetch_and_clean_up {
git
"remote update -p origin"
;
git
"gc --auto"
;
return
;
}
sub
git_tag {
our
%decorations
;
if
(
$_
[0] eq
'-d'
) {
my
(
$d
,
$name
) =
@_
;
my
$commit
= git
"rev-parse $name"
;
git
"tag $d $name"
;
delete
$decorations
{
$commit
}{
"refs/tags/$name"
};
}
else
{
my
(
$f
,
$name
,
$commit
) =
@_
== 3 ?
@_
: (
''
,
@_
);
$commit
= git
"rev-parse $commit"
if
$commit
eq
'HEAD'
;
git
"tag $f $name $commit"
;
$decorations
{
$commit
}{
"refs/tags/$name"
} = 1;
}
return
;
}
sub
highest_quickfix_number {
my
(
$project_name
) =
@_
;
git
'fetch origin --tags'
;
my
@quickfixes
=
grep
{m|^meta/quickfix|} get_meta_tags();
my
@numbers
=
map
{ /quickfix(\d+)$/ ? $1 : () }
@quickfixes
;
return
0
if
not
@numbers
;
return
max
@numbers
;
}
sub
history {
my
(
$project_name
,
$changeset
)
=
@_
== 2 ?
@_
: ( project_name(),
@_
);
my
$changeset_exists
=
grep
{m|^meta/
$changeset
|} get_meta_tags();
return
[]
unless
$changeset_exists
;
my
$events
= view_blob(
"meta/$changeset"
);
for
(
@$events
) {
my
@lt
=
localtime
$_
->{stamp};
$_
->{stamp} = strftime(
"%Y-%m-%d %T"
,
@lt
);
}
return
wantarray
?
@$events
:
$events
;
}
sub
history_owner {
my
$history
=
shift
;
my
$open
= first {
$_
->{action} eq
'open'
}
@$history
;
return
$open
->{user};
}
sub
history_reviewer {
my
$history
=
shift
;
my
$last
;
$last
= first {
$_
->{action} eq
'submit'
}
reverse
@$history
;
return
if
not
$last
;
return
$last
->{reviewer};
}
sub
history_status {
my
$history
=
shift
;
my
$last
= first {
$_
->{action} !~ m/^(touch|promote|demote)$/ }
reverse
@$history
;
my
$action
=
$last
->{action};
return
{
open
=>
'open'
,
submit
=>
'submitted'
,
review
=>
'reviewing'
,
fail
=>
'failed'
,
pass
=>
'merged'
,
edit
=>
'open'
,
}->{
$action
};
}
sub
history_submitter {
my
(
$history
) =
@_
;
my
$last
= first {
$_
->{action} eq
'submit'
}
reverse
@$history
;
return
if
not
$last
;
return
$last
->{user};
}
sub
is_auto_fetch {
my
$config
= git_config();
my
$value
=
$config
->{gitc}{fetch} ||
'auto'
;
return
$value
eq
'auto'
;
}
sub
is_merge_commit {
my
(
$ref
) =
@_
;
my
(
$parents
) = git
"log -1 --no-color --pretty=format:%P $ref"
;
return
if
not
$parents
;
my
@parents
=
split
/ /,
$parents
;
return
@parents
> 1;
}
sub
is_suspendable {
our
$suspend_file
=
'.git/gitc-suspended-process'
;
open
my
$fh
,
'>'
,
$suspend_file
or
die
"Unable to create $suspend_file: $!\n"
;
print
$fh
"$$\n"
;
my
$command
= command_name();
print
$fh
"gitc $command is suspended. Resume it with 'fg'\n"
;
our
$is_suspendable
= 1;
close
$fh
;
}
END {
our
$is_suspendable
;
our
$suspend_file
;
unlink
$suspend_file
if
$is_suspendable
and -e
$suspend_file
;
}
sub
is_valid_ref {
my
(
$name
) =
@_
;
return
if
not
defined
$name
;
my
$sha1
=
eval
{ git
"rev-parse --verify --quiet $name"
};
return
$sha1
if
$sha1
;
return
;
}
sub
open_packed_refs {
my
(
$prefix
) =
@_
;
if
( not
defined
$prefix
) {
Carp::croak(
"open_packed_refs requires a prefix argument"
);
}
my
$git_dir
= git_dir();
my
$packed_refs
=
"$git_dir/packed-refs"
;
return
if
not -e
$packed_refs
;
open
my
$old_fh
,
'<'
,
$packed_refs
or
die
"Can't open $packed_refs: $!"
;
my
$header
= <
$old_fh
>;
my
(
$technique
) =
$header
=~ /^
$technique
||=
''
;
die
"Unknown ref packing technique: $technique\n"
if
$technique
ne
'peeled'
;
my
(
$new_fh
,
$new_filename
)
= File::Temp::tempfile(
"$prefix-XXXX"
,
DIR
=>
$git_dir
);
print
$new_fh
$header
;
return
(
$old_fh
,
$new_fh
,
$new_filename
);
}
sub
parse_changeset_spec {
my
(
$spec
) =
@_
;
if
( not
defined
$spec
) {
my
$changeset
= current_branch();
my
$project
= project_name();
return
(
$project
,
$changeset
);
}
return
( $1, $2 )
if
$spec
=~ m/^(.*)
my
$project
= project_name();
die
"Unable to determine the project for changeset spec '$spec'.\n"
.
"You either need to be inside a gitc repository or specify\n"
.
"the full changeset name like project#changeset\n"
if
not
$project
;
return
(
$project
,
$spec
);
}
sub
project_name {
our
$project_name
;
return
$project_name
if
defined
$project_name
;
my
(
$line
) = git
"show HEAD:.gitc"
;
die
"You need to specify a project name in a .gitc file. See the HOWTO for more details."
unless
$line
;
my
(
$name
) =
$line
=~ m/^\s
*name
\s*:\s*(.*)$/;
return
$project_name
=
$name
;
}
sub
project_root {
my
$git_dir
= git_dir();
if
( not
$git_dir
=~ s{/.git$}{} ) {
Carp::croak(
"Bare repositories don't have a meaningful project root"
);
}
return
$git_dir
;
}
sub
remote_branch_exists {
my
(
$branch
) =
@_
;
my
@remote_branches
= git
"branch --no-color -r"
;
return
scalar
grep
{
$_
eq
" origin/$branch"
}
@remote_branches
}
sub
sendmail {
my
(
$args
) =
@_
;
my
$recipient
=
$args
->{to} ||
die
"No mail recipient"
;
my
$subject
=
$args
->{subject} ||
die
"No mail subject"
;
my
$content
=
$args
->{content} ||
q{}
;
my
$link
=
$args
->{
link
} ||
q{}
;
my
$project
=
$args
->{project} || project_name();
my
$changeset
=
$args
->{changeset} ||
die
"No mail changeset ID"
;
my
$command
=
eval
{ command_name() } ||
'unknown'
;
my
$extra_headers
=
''
;
my
(
$temp_fh
,
$temp_file
)
= File::Temp::tempfile(
"gitc-$command-XXXX"
,
UNLINK
=> 1 );
my
$name
= get_user_name() .
' <'
. get_user_email() .
'>'
;
print
$temp_fh
<<ENDMAIL;
To: $recipient
From: $name
Subject: [$project#$changeset] $subject
$extra_headers
$link
$content
ENDMAIL
close
$temp_fh
or
die
"Couldn't close temporary file for mail"
;
let_user_edit(
$temp_file
)
if
-t STDOUT;
die
"Aborting at user's request\n"
if
-s
$temp_file
<= 10;
my
$sendmail
= find_sendmail();
my
$send_it
=
sub
{
system
qq($sendmail -t < $temp_file)
};
return
$send_it
if
$args
->{lazy};
$send_it
->();
return
;
}
sub
find_sendmail {
my
$sendmail
=
'/usr/sbin/sendmail'
;
( -x
$sendmail
) or (
$sendmail
=
'/usr/lib/sendmail'
);
( -x
$sendmail
) or (
$sendmail
=
'sendmail'
);
unless
(-x
$sendmail
) {
for
my
$dir
(File::Spec->path) {
if
( -x
"$dir/sendmail"
) {
$sendmail
=
"$dir/sendmail"
;
last
;
}
}
}
unless
(-x
$sendmail
) {
die
"Couldn't find an executable sendmail"
;
}
return
$sendmail
;
}
sub
short_ref_name {
my
(
$ref
) =
@_
;
return
if
not
defined
$ref
;
my
$name
=
qr{[^/]+}
o;
my
@patterns
= (
qr{cs/($name)/head}
o,
qr{origin/pu/($name)}
o,
qr{^($name)$}
o,
);
for
my
$pattern
(
@patterns
) {
return
$1
if
$ref
=~
$pattern
;
}
return
;
}
sub
sort_changesets_by_name {
my
(
$ids
) =
@_
;
@$ids
=
map
{
$_
->[0] }
sort
{
$a
->[1] cmp
$b
->[1]
or
$a
->[2] <=>
$b
->[2]
or
$a
->[3] cmp
$b
->[3]
}
map
{
m/^(\D+)(\d+)(\D*)$/ ? [
$_
, $1, $2, $3 ]
: [
$_
,
$_
, 999_999,
''
]
}
@$ids
;
return
;
}
sub
split_decorations {
my
(
$decorations
) =
@_
;
return
if
not
defined
$decorations
;
return
if
length
(
$decorations
) < 4;
$decorations
=
substr
$decorations
, 2, -1;
return
split
/, /,
$decorations
;
}
sub
toplevel {
chomp
(
my
$top
=
qx{git rev-parse --show-toplevel}
);
unless
(
$top
) {
die
'Not a git repository (or any of the parent directories): .git'
;
}
return
$top
;
}
sub
traverse_commits {
my
(
$git_log_arguments
,
$callback
) =
@_
;
open
my
$git
,
'-|'
,
"git log --no-color --pretty=raw $git_log_arguments"
or
die
;
my
@commit_lines
;
my
@accumulator
;
my
$finished
= 0;
COMMIT:
while
( not
$finished
) {
while
(1) {
my
$line
= <
$git
>;
if
( not
defined
$line
) {
@commit_lines
=
@accumulator
;
@accumulator
= ();
$finished
= 1;
last
;
}
chomp
$line
;
if
(
@accumulator
and
$line
=~ m/^commit / ) {
@commit_lines
=
@accumulator
;
@accumulator
= (
$line
);
last
;
}
push
@accumulator
,
$line
;
}
last
if
not
@commit_lines
;
my
(
$commit
) =
$commit_lines
[0] =~ /^commit (\S+)/;
my
@parents
=
map
{ /^parent (.*)$/ ? $1 : () }
@commit_lines
[2,3];
die
"Unable to locate commit"
if
not
$commit
;
die
"Unable to locate parents"
if
not
@parents
and not
$finished
;
$callback
->({
commit
=>
$commit
,
parents
=> \
@parents
,
message
=> [
@commit_lines
[ 6 ..
$#commit_lines
] ],
});
}
return
;
}
sub
unmerged_changesets {
my
(
$project_name
) =
@_
;
my
@meta_tags
= get_meta_tags();
my
@unmerged
;
for
my
$tag
(
@meta_tags
) {
my
$meta_info
= view_blob(
$tag
);
my
$passed
;
for
my
$entry
(
@$meta_info
) {
++
$passed
if
$entry
->{action} eq
'pass'
;
}
push
@unmerged
,
@$meta_info
unless
$passed
;
}
my
%result
;
for
my
$event
(
sort
{
$a
->{stamp} <=>
$b
->{stamp}}
@unmerged
) {
my
@lt
=
localtime
$event
->{stamp};
$event
->{stamp} = strftime(
"%Y-%m-%d %T"
,
@lt
);
push
@{
$result
{
$event
->{changeset} } },
$event
;
}
return
\
%result
;
}
sub
unpromoted {
my
(
$from
,
$to
) =
@_
;
$from
= [
$from
]
if
not
ref
$from
;
$to
= [
$to
]
if
not
ref
$to
;
my
$backstop
= backstop_commit(
$from
,
$to
);
my
@source_changes
= changesets_in(
$from
,
$backstop
);
my
@target_changes
= changesets_in(
$to
,
$backstop
);
return
_missing_changesets( \
@source_changes
, \
@target_changes
);
}
our
$meta_cache
;
sub
cache_meta_data {
my
(
@refs
) =
@_
;
@refs
= get_meta_tags(
fetch
=> 0)
unless
@refs
;
@refs
=
map
{m|^meta/| ?
$_
:
"meta/$_"
}
@refs
;
push
@$meta_cache
,
map
{ {
$_
=> git
"rev-parse $_"
} }
@refs
;
return
;
}
sub
restore_meta_data {
our
$meta_cache
;
die
"You cannot restore meta data without caching any data"
unless
$meta_cache
;
git_tag(
'-d'
,
$_
)
for
map
{
keys
%$_
}
@$meta_cache
;
git_tag(
%$_
)
for
@$meta_cache
;
git
sprintf
"push --force origin %s"
,
join
' '
,
map
{
keys
%$_
}
@$meta_cache
;
undef
$meta_cache
;
return
;
}
sub
version_tag_prefix {
my
(
$branch
) =
@_
;
return
"version/$branch/"
;
}
sub
backstop_commit {
return
is_valid_ref(
'cvs'
);
}
sub
changesets_in {
my
(
$commits
,
$backstop
) =
@_
;
my
@command
=
qw( git log --no-color --first-parent --topo-order --pretty=format:%H )
;
push
@command
,
" ^$backstop"
if
$backstop
;
open
my
$log
,
"-|"
,
@command
,
@$commits
or
die
;
my
$env
=
qr/(?:master|test|stage|prod)/
;
my
$cs
=
qr{[^/]+}
;
my
@rxen
= (
qr{^refs/tags/cs/($cs)/to-$env$}
o,
qr{^refs/tags/cs/($cs)/head$}
o,
qr{^refs/remotes/origin/pu/(.+)$}
o,
);
my
@included
;
my
%seen
;
while
(
my
$commit
= <
$log
> ) {
chomp
$commit
;
for
my
$name
( commit_decorations(
$commit
) ) {
for
my
$rx
(
@rxen
) {
if
(
$name
=~
$rx
) {
my
$changeset
= $1;
next
if
$seen
{
$changeset
}++;
push
@included
,
$changeset
;
}
elsif
(
$name
=~ m{^refs/tags/cs/(
$cs
)/rm-
$env
$}o ) {
my
$changeset
= $1;
next
if
$seen
{
$changeset
}++;
}
}
}
}
return
@included
;
}
sub
_missing_changesets {
my
(
$source_changes
,
$target_changes
) =
@_
;
my
%source
=
map
{
$_
=> 1 }
@$source_changes
;
my
%target
=
map
{
$_
=> 1 }
@$target_changes
;
delete
@source
{
keys
%target
};
return
grep
{
$source
{
$_
} }
@$source_changes
;
}
sub
command_name {
my
(
$command
) = $0 =~ m{/gitc-(\w+)$};
return
$command
if
$command
;
die
"Unable to determine the command name from $0\n"
;
}
sub
_states {
my
(
$self
,
$command
,
$target
) =
@_
;
my
$statuses
= its_config()->{
$command
}
or
die
'No '
. its->label_service .
" statuses for $command"
;
if
( not
$target
) {
die
"No initial status"
unless
$statuses
->{from};
die
"No final status"
unless
$statuses
->{to};
return
(
$statuses
);
}
die
"No initial status for target $target"
unless
$statuses
->{
$target
}{from};
die
"No final status for target $target"
unless
$statuses
->{
$target
}{to};
return
$statuses
->{
$target
};
}
sub
state_blocked {
my
(
$command
,
$state
) =
@_
;
my
$statuses
= its_config()->{
$command
}
or
die
'No '
. its()->label_service .
" statuses for $command"
;
my
$block
=
$statuses
->{ block };
return
unless
$block
;
return
1
if
any {
warn
" \$_: $_, \$state: $state.\n"
;
$_
eq
$state
} @{
$block
};
return
;
}
1;