#!/usr/bin/perl
my
$COMPACT
= 1;
my
$GAP
= 2;
my
$SPLIT_MERGE
= 1;
my
$VERBOSE
= 0;
my
$COLOR
= 1;
my
%opt
= (
'compact'
=> \
$COMPACT
,
'gap'
=> \
$GAP
,
'split-merge'
=> \
$SPLIT_MERGE
,
'verbose'
=> \
$VERBOSE
,
'color'
=> \
$COLOR
,
);
foreach
my
$k
(
qw/compact split-merge color/
) {
my
$v
= `git config fancygraph.
$k
`;
chomp
$v
;
if
(
$v
ne
''
) {
if
(
$v
=~ /^yes$/i ) {
${
$opt
{
$k
}} = 1;
}
elsif
(
$v
=~ /^
no
$/i ) {
${
$opt
{
$k
}} = 0;
}
else
{
print
"Invalid option fancygraph.$k = [$v]\n"
;
exit
1;
}
}
}
GetOptions(
\
%opt
,
'help|?'
=>
sub
{ pod2usage(
-verbose
=> 1); },
'man'
=>
sub
{ pod2usage(
-verbose
=> 2,
-noperldoc
=> 1); },
'compact!'
,
'gap=i'
,
'split-merge!'
,
'verbose'
,
'color!'
,
'no-msg|nomsg|no-message|nomessage'
,
) or pod2usage(2);
local
$| = 1
if
$VERBOSE
;
my
$git_option
=
'--all'
;
if
(
@ARGV
) {
$git_option
=
join
' '
,
@ARGV
;
}
my
%commit
= ();
my
@timeline
= ();
my
$pat_color
=
qr/(?:\e\[[;\d]*?m)/
;
my
$pat_sha_id
=
qr/$pat_color?[0-9a-fA-F]{7}$pat_color?/
;
my
$pat_gitlog_line
=
qr/^
((?:\s*?$pat_sha_id)+) # sha-id's
\s+
($pat_color?.+?$pat_color?) # source
\s
(.+) # commit message
$
/
x;
my
@merge_commits
= ();
my
%print
= ();
my
$usedcolumn
= 0;
my
@returnedcolumn
= ();
{
my
$color
= 0;
sub
next_color {
$color
= ((
$color
+1)%6);
$color
++
if
$color
== 2;
return
1 +
$color
;
}
}
sub
assign_col {
my
(
$src
,
$idx
) =
@_
;
if
(
$COMPACT
and
@returnedcolumn
) {
for
(
my
$i
=0;
$i
<
@returnedcolumn
;
$i
++) {
if
(
$returnedcolumn
[
$i
][1] >
$idx
+1 ) {
my
$ret
=
$returnedcolumn
[
$i
][0];
splice
@returnedcolumn
,
$i
, 1;
verbose(
" we can use col [$ret]\n"
);
return
$ret
;
}
}
}
verbose(
" new column number [$usedcolumn]\n"
);
return
$usedcolumn
++;
}
sub
max_col {
return
$usedcolumn
- 1;
}
sub
free_col {
my
(
$col
,
$idx
) =
@_
;
push
@returnedcolumn
, [
$col
,
$idx
];
return
;
}
sub
colored_symbol {
my
(
$sym
,
$color
) =
@_
;
return
"\e[${color}m$sym\e[m"
if
$COLOR
;
return
$sym
;
}
sub
rename_src {
my
(
$root
,
$old
,
$new
,
$ca
) =
@_
;
return
unless
exists
$commit
{
$root
};
my
@stack
= (
$root
);
while
(
@stack
) {
my
$id
=
shift
@stack
;
if
(
exists
$commit
{
$id
} and
$commit
{
$id
}{
'src'
} eq
$old
) {
verbose(
" rename [$id] to [$new]\n"
);
$commit
{
$id
}{
'src'
} =
$new
;
}
else
{
next
;
}
foreach
my
$p
( @{
$commit
{
$id
}{
'parents'
}} ) {
next
if
(
$p
eq
$ca
);
push
@stack
,
$p
;
}
}
}
sub
verbose {
my
$str
=
shift
;
return
unless
$VERBOSE
;
$str
=~ s/
$pat_color
//g
unless
$COLOR
;
print
$str
;
}
verbose(
"PHASE 1 : read git log with '--parents'...\n"
);
{
my
$idx
= 0;
open
my
$git
,
"-|"
,
"git log --oneline --decorate --color=always --source --parents --date-order $git_option"
;
while
(
my
$line
= <
$git
>) {
chomp
$line
;
if
(
$line
=~ /
$pat_gitlog_line
/ ) {
my
(
$sha_block_p
,
$src_p
,
$msg
) = ( $1, $2, $3, $4 );
$sha_block_p
=~ s/^\s+//;
my
@sha_block_p
=
split
/\s+/,
$sha_block_p
;
(
my
$sha_block
=
$sha_block_p
) =~ s/
$pat_color
//g;
my
@sha_block
=
split
/\s+/,
$sha_block
;
my
$sha_id
=
shift
@sha_block
;
my
$sha_id_p
=
shift
@sha_block_p
;
$src_p
=~ s{^(
$pat_color
?)refs/}{$1}g;
(
my
$src
=
$src_p
) =~ s/
$pat_color
//g;
@{
$commit
{
$sha_id
}}{
qw/idx sha_id_p msg src src_p parents/
} =
( ++
$idx
,
$sha_id_p
,
$msg
,
$src
,
$src_p
, [
@sha_block
] );
if
( not
exists
$commit
{
$sha_id
}{
'children'
} ) {
$commit
{
$sha_id
}{
'children'
} = [ ];
}
push
@timeline
,
$sha_id
;
verbose(
" add commit [$idx][$sha_id][$src][$msg]\n"
);
foreach
my
$id
(
@sha_block
) {
push
@{
$commit
{
$id
}{
'children'
}},
$sha_id
;
}
if
(2 <= @{
$commit
{
$sha_id
}{
'parents'
}}) {
$commit
{
$sha_id
}{
'merge'
} = 1;
push
@merge_commits
,
$commit
{
$sha_id
};
}
}
}
close
$git
;
}
verbose(
"PHASE 1 : done.\n\n\n"
);
foreach
my
$id
(
keys
%commit
) {
unless
(
exists
$commit
{
$id
}{
'idx'
} ) {
delete
$commit
{
$id
};
}
}
if
(
$SPLIT_MERGE
) {
verbose(
"PHASE 2 : rename merged commits...\n"
);
my
%lastnum
= ();
foreach
my
$cmt
(
@merge_commits
) {
if
(
$VERBOSE
) {
(
my
$cmtid
=
$cmt
->{
'sha_id_p'
} ) =~ s/
$pat_color
//g;
print
"Check merge commit [$cmtid].....\n"
;
}
my
$src
=
$cmt
->{
'src'
};
my
@parents
= @{
$cmt
->{
'parents'
}};
my
$first_id
=
shift
@parents
;
my
$basename
;
if
(
$src
=~ /^(.+?)(?:'(\d+))?$/ ) {
$basename
= $1;
if
( not
exists
$lastnum
{
$basename
} ) {
$lastnum
{
$basename
} = 2;
}
}
else
{
die
"Assertion failed: branch name [$src]"
;
}
foreach
my
$p_id
(
@parents
) {
next
unless
exists
$commit
{
$p_id
};
if
(
$src
eq
$commit
{
$p_id
}{
'src'
} ) {
my
$common_ancestor
= `git merge-base
$first_id
$p_id
`;
$common_ancestor
=
substr
(
$common_ancestor
, 0, 7);
my
$newsrc
=
$basename
.
"'"
.
$lastnum
{
$basename
};
$lastnum
{
$basename
}++;
verbose(
"rename commits from [$p_id] before [$common_ancestor] as [$newsrc]\n"
);
rename_src(
$p_id
,
$src
,
$newsrc
,
$common_ancestor
);
}
}
}
verbose(
"PHASE 2 : done.\n\n\n"
);
}
verbose(
"PHASE 3 : assign column and color to each branch...\n"
);
{
my
$conf
= `git config fancygraph.fixcolumn`;
chomp
$conf
;
foreach
my
$src
(
split
/\s+/,
$conf
) {
$print
{
"heads/$src"
}{
'column'
} = assign_col(
$src
, 0);
$print
{
"heads/$src"
}{
'color'
} = next_color();
}
}
my
$last_color
= -1;
foreach
my
$id
(
reverse
@timeline
) {
my
$cmt
=
$commit
{
$id
};
my
$src
=
$cmt
->{
'src'
};
if
( not
defined
$print
{
$src
} ) {
my
$bottom
=
$cmt
->{
'idx'
};
foreach
my
$id
( @{
$cmt
->{
'parents'
}} ) {
next
unless
exists
$commit
{
$id
};
if
(
$cmt
->{
'idx'
} ==
@timeline
) {
next
; }
if
(
$bottom
<
$commit
{
$id
}{
'idx'
} ) {
$bottom
=
$commit
{
$id
}{
'idx'
};
}
}
my
$new_col
= assign_col(
$src
,
$bottom
);
$print
{
$src
}{
'column'
} =
$new_col
;
verbose(
" assign column [$new_col] to [$id][$cmt->{msg}] / [$src]\n"
);
$print
{
$src
}{
'color'
} = next_color();
while
(
$print
{
$src
}{
'color'
} ==
$last_color
) {
$print
{
$src
}{
'color'
} = next_color();
}
}
if
(not
grep
{
exists
$commit
{
$_
} and
$src
eq
$commit
{
$_
}{
'src'
} } @{
$cmt
->{
'children'
}}) {
$cmt
->{
'head'
} = 1;
my
$top
=
$cmt
->{
'idx'
};
foreach
my
$id
( @{
$cmt
->{
'children'
}} ) {
next
unless
exists
$commit
{
$id
};
if
(
$commit
{
$id
}{
'merge'
} and
$top
>
$commit
{
$id
}{
'idx'
} ) {
$top
=
$commit
{
$id
}{
'idx'
};
}
}
verbose(
" free column [$print{$src}{column}] at index [$top]\n"
);
free_col(
$print
{
$src
}{
'column'
},
$top
);
}
$last_color
=
$print
{
$src
}{
'color'
};
}
verbose(
"PHASE 3 : done.\n\n\n"
);
{
my
$HEAD_id
= `git rev-list -1 HEAD`;
$HEAD_id
=
substr
(
$HEAD_id
, 0, 7);
my
$idx
= 0;
open
my
$less
,
'|-'
,
'less -RFfX'
;
my
$maxc
= 1 + max_col();
my
@nextline
= (
' '
)x(
$GAP
*$maxc
);
foreach
my
$id
(
@timeline
) {
my
@currentline
=
@nextline
;
$idx
++;
my
$cmt
=
$commit
{
$id
};
my
$prt
=
$print
{
$cmt
->{
'src'
}};
my
$color
=
'3'
.
$prt
->{
'color'
};
my
$symbol
;
if
(
$cmt
->{merge} ) {
$symbol
=
'M'
;
}
else
{
$symbol
=
'O'
;
}
if
(
$cmt
->{head} ) {
$symbol
= colored_symbol(
$symbol
, 103);
}
my
$indent
=
$GAP
*
$prt
->{
'column'
};
$currentline
[
$indent
] = colored_symbol(
$symbol
,
$color
);
if
( @{
$cmt
->{parents}} ) {
if
(
grep
{
exists
$commit
{
$_
} } @{
$cmt
->{parents}} ) {
$nextline
[
$indent
] = colored_symbol(
'|'
,
$color
);
}
else
{
$nextline
[
$indent
] = colored_symbol(
'^'
,
$color
);
}
}
else
{
$nextline
[
$indent
] =
' '
;
}
foreach
my
$s
( @{
$cmt
->{children}} ) {
next
if
( not
exists
$commit
{
$s
} );
my
$c
=
$commit
{
$s
};
my
$b
=
$c
->{
'src'
};
next
if
(
$cmt
->{
'src'
} eq
$b
);
next
if
(
$c
->{
'merge'
} );
my
$col
=
$GAP
*$print
{
$b
}{
'column'
};
$currentline
[
$col
] = colored_symbol(
'^'
,
'3'
.
$print
{
$b
}{
'color'
});
foreach
my
$i
(
$indent
<
$col
? (
$indent
+1 ..
$col
-1 ) : (
$col
+1 ..
$indent
-1 ) ) {
if
(
$currentline
[
$i
] =~ /[ |]/ ) {
$nextline
[
$i
] =
$currentline
[
$i
];
$currentline
[
$i
] = colored_symbol(
'-'
,
$color
);
}
}
}
for
(
my
$i
=0;
$i
<
@currentline
;
$i
++ ) {
$nextline
[
$i
] =
' '
if
$currentline
[
$i
] =~ /\^/;
$nextline
[
$i
] =
$currentline
[
$i
]
if
$currentline
[
$i
] =~ /\|/;
}
printf
{
$less
}
"%5d. "
,
$idx
if
$VERBOSE
;
print
{
$less
}
join
(
''
,
@currentline
);
(
my
$tmp_src
=
$cmt
->{
'src'
}) =~ s{^(.).*?/}{($1) };
(
my
$tmp_msg
=
$cmt
->{
'msg'
}) =~ s{\((
$pat_color
)}{colored_symbol(
'('
, 33).$1}e;
my
$line
=
''
;
if
(
$id
eq
$HEAD_id
) {
$line
.= colored_symbol(
'*'
.
$cmt
->{
'sha_id_p'
}, 103);
}
else
{
$line
.=
' '
.
$cmt
->{
'sha_id_p'
};
}
$line
.=
" "
. colored_symbol(
$tmp_src
,
$color
);
$line
.=
" "
.
$tmp_msg
unless
$opt
{
'no-msg'
};
$line
=~ s/
$pat_color
//g
unless
$COLOR
;
print
{
$less
}
$line
;
print
{
$less
}
"\n"
;
if
(
$cmt
->{
'merge'
} and
$cmt
->{
'idx'
} !=
@timeline
) {
my
@templine
= (
' '
)x(
$GAP
*$maxc
);
for
(
my
$i
=0;
$i
<
@currentline
;
$i
++) {
$templine
[
$i
] =
$nextline
[
$i
]
if
$nextline
[
$i
] =~ /[|]/;
}
$templine
[
$indent
] = colored_symbol(
'+'
,
$color
);
if
( not
grep
{
exists
$commit
{
$_
} and
$cmt
->{
'src'
} eq
$commit
{
$_
}{
'src'
} } @{
$cmt
->{parents}} ) {
$nextline
[
$indent
] =
' '
;
}
my
$col_diff
=
sub
{
my
$id
=
shift
;
return
abs
(
$indent
-
$print
{
$commit
{
$id
}{
'src'
}}{
'column'
} );
};
foreach
my
$s
(
sort
{
$col_diff
->(
$b
) <=>
$col_diff
->(
$a
) }
grep
{
exists
$commit
{
$_
} } @{
$cmt
->{
'parents'
}} ) {
my
$c
=
$commit
{
$s
};
my
$b
=
$c
->{
'src'
};
next
if
(
$cmt
->{
'src'
} eq
$b
);
my
$temp_color
=
'3'
.
$print
{
$b
}{
'color'
};
my
$bcol
=
$GAP
*$print
{
$b
}{
'column'
};
$templine
[
$bcol
] = colored_symbol(
'.'
,
$temp_color
);
$nextline
[
$bcol
] = colored_symbol(
'|'
,
$temp_color
);
foreach
my
$i
(
$indent
<
$bcol
? (
$indent
+1 ..
$bcol
-1 ) : (
$bcol
+1 ..
$indent
-1 ) ) {
$templine
[
$i
] = colored_symbol(
'-'
,
$temp_color
);
}
}
printf
{
$less
}
"%7s"
,
''
if
$VERBOSE
;
print
{
$less
}
join
(
''
,
@templine
),
"\n"
;
}
}
close
$less
;
}