summaryrefslogtreecommitdiff
path: root/git2log
diff options
context:
space:
mode:
Diffstat (limited to 'git2log')
-rwxr-xr-xgit2log945
1 files changed, 945 insertions, 0 deletions
diff --git a/git2log b/git2log
new file mode 100755
index 0000000..02b6fcb
--- /dev/null
+++ b/git2log
@@ -0,0 +1,945 @@
+#! /usr/bin/perl
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#
+# This script is maintained at https://github.com/openSUSE/linuxrc-devtools
+#
+# If you're in another project, this is just a copy.
+# You may update it to the latest version from time to time...
+#
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+use strict;
+
+use Getopt::Long;
+
+use Data::Dumper;
+$Data::Dumper::Sortkeys = 1;
+$Data::Dumper::Terse = 1;
+$Data::Dumper::Indent = 1;
+
+sub usage;
+sub changelog_outdated;
+sub get_github_project;
+sub get_version;
+sub get_tags;
+sub get_log;
+sub is_formatted_tag;
+sub get_branch;
+sub choose_tags;
+sub add_head_tag;
+sub tags_to_str;
+sub format_log;
+sub format_all_logs;
+sub fix_dates;
+sub add_line_breaks;
+sub format_date_obs;
+sub format_date_iso;
+sub raw_date_to_s;
+
+usage 0 if !@ARGV;
+
+my @changelog_deps = qw ( .git/HEAD .git/refs/heads .git/refs/tags );
+
+my $branch;
+my $current_version;
+my @tags;
+my @all_tags;
+my $config;
+
+my $opt_log;
+my $opt_version;
+my $opt_branch;
+my $opt_update;
+my $opt_file;
+my $opt_start;
+my $opt_max;
+my $opt_width = 66;
+my $opt_width_fuzz = 8;
+my $opt_sep_width = 68;
+my $opt_format = 'internal'; # obs, internal
+my $opt_merge_msg_before = 1; # log auto generated pr merge message before the commit messages (vs. after)
+my $opt_join_author = 1; # join consecutive commit messages as long as they are by the same author
+my $opt_keep_date = 1; # don't join consecutive commit messages if they have different time stamps
+my $opt_default_email = 'opensuse-packaging@opensuse.org'; # default email to use in changelog
+
+GetOptions(
+ 'help' => sub { usage 0 },
+ 'version' => \$opt_version,
+ 'branch' => \$opt_branch,
+ 'update' => \$opt_update,
+ 'start=s' => \$opt_start,
+ 'format=s' => \$opt_format,
+ 'max=i' => \$opt_max,
+ 'width=i' => \$opt_width,
+ 'fuzz=i' => \$opt_width_fuzz,
+ 'merge-msg=s' => sub { $opt_merge_msg_before = ($_[1] eq 'after' ? 0 : 1) },
+ 'join-author!' => \$opt_join_author,
+ 'keep-date!' => \$opt_keep_date,
+ 'log|changelog' => \$opt_log,
+ 'default-email=s' => \$opt_default_email,
+) || usage 1;
+
+# ensure we are used correctly
+usage 1 if @ARGV > 1 || !($opt_log || $opt_version || $opt_branch);
+$opt_file = @ARGV ? shift : '-';
+
+die "no git repo\n" unless -d ".git";
+
+# if update option has been give write changelog only if git refs are newer
+exit 0 if $opt_update && $opt_file ne '-' && -f($opt_file) && !changelog_outdated($opt_file);
+
+$opt_max = 2 if $opt_version || $opt_branch;
+
+# gather some data
+get_github_project;
+get_branch;
+get_log;
+fix_dates;
+get_tags;
+choose_tags;
+add_head_tag;
+get_version;
+
+# just print current branch
+if($opt_branch) {
+ open my $f, ">$opt_file";
+ print $f $config->{branch} ? $config->{branch} : "master", "\n";
+ close $f;
+
+ exit 0;
+}
+
+# just print current version
+if($opt_version) {
+ my $old_version;
+
+ if($opt_file ne '-' && open(my $f, $opt_file)) {
+ chomp($old_version = <$f>);
+ close $f;
+ }
+
+ if($config->{version} ne $old_version) {
+ open my $f, ">$opt_file";
+ print $f "$config->{version}\n";
+ close $f;
+ }
+
+ exit 0;
+}
+
+# set start tag
+if($opt_start) {
+ my $x = is_formatted_tag $opt_start;
+ die "$opt_start: not a valid start tag\n" if !$x;
+ $x->{branch} = $config->{branch} if !$x->{branch};
+ $config->{start} = $x;
+}
+
+format_all_logs;
+
+open my $f, ">$opt_file";
+
+print $f $_->{formatted} for @{$config->{log}};
+
+close $f;
+
+exit 0;
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# usage(exit_code)
+#
+# Print help message and exit.
+# - exit_code: exit code
+#
+# Function does not return.
+#
+sub usage
+{
+ my $err = shift;
+
+ print <<" usage";
+Usage: git2log [OPTIONS] [FILE]
+Create changelog and project version from git repo.
+ --changelog Write changelog to FILE.
+ --version Write version number to FILE.
+ --branch Write current branch to FILE.
+ --start START_TAG Start with tag START_TAG.
+ --max N Write at most MAX long entries.
+ --update Write changelog or version only if FILE is outdated.
+ --format FORMAT Write log using FORMAT. Supported FORMATs are 'internal' (default) and 'obs'.
+ --width WIDTH Reformat log entries to be max WIDTH chars wide.
+ --fuzz FUZZ Allow log lines to be up to FUZZ chars longer as WIDTH to avoid
+ line breaks leaving tiny bits on the last line.
+ --merge-msg WHERE Log message about merges before or after the actual merge commit messages.
+ Valid values for WHERE are 'after' and 'before' (default).
+ --join-author Join consecutive commits as long as they are by the same author. (default)
+ --no-join-author Keep consecutive commits by the same author separate.
+ --keep-date Join consecutive commits only if they have the same date. (default)
+ --no-keep-date Join consecutive commits even if dates differ.
+ --default-email Use this email in changelog entries if no other suitable email could be
+ determined (default: opensuse-packaging\@opensuse.org).
+ --help Print this help text.
+ usage
+
+ exit $err;
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# res = changelog_outdated(file)
+#
+# Return status of changelog file.
+# - file: changelog file name
+# - res: status
+# 1: file is newer than the last git repo change and should be updated
+# 0: file is still recent enough
+#
+# Relies on global var @changelog_deps.
+#
+sub changelog_outdated
+{
+ my $file = $_[0];
+
+ my $changelog_time = (stat $file)[9];
+
+ return 1 if !defined $changelog_time;
+
+ for (@changelog_deps) {
+ return 1 if (stat)[9] > $changelog_time;
+ }
+
+ return 0;
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# get_github_project()
+#
+# Set $config->{github_project} to the github project name.
+#
+sub get_github_project
+{
+ if(`git config remote.origin.url` =~ m#github.com[:/]+(\S+/\S+)#) {
+ $config->{github_project} = $1;
+ $config->{github_project} =~ s/\.git$//;
+ }
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# get_version()
+#
+# Set $config->{branch} and $config->{version} to the current branch and
+# version info.
+#
+# This might be taken directly from HEAD if HEAD is tagged or otherwise be
+# exprapolated from the most recent tag (cf. add_head_tag()).
+#
+sub get_version
+{
+ $config->{version} = "0.0";
+
+ my $tag = $config->{log}[0]{tags}[0];
+
+ if($tag->{version}) {
+ $config->{version} = $tag->{version};
+ $config->{branch} = $tag->{branch};
+ }
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# get_tags()
+#
+# Parse $config->{raw_log}, extract tag names, and split into per-tag
+# sections.
+#
+# Only tags recognized by is_formatted_tag() are considered.
+#
+# The parsed logs is stored in $config->{log}, an array of log sections.
+# Each section is a hash with these keys:
+# - 'tags': array of tags for this section
+# - 'commit': git commit id associated with these tags
+# - 'lines': git log lines
+#
+sub get_tags
+{
+ my $log_entry;
+
+ for (@{$config->{raw_log}}) {
+ if(/^commit (\S+)( \((.*)\))?/) {
+ my $commit = $1;
+ my $tag_list = $3;
+ my $xtag;
+
+ for my $t (split /, /, $tag_list) {
+ if($t =~ /tag: (\S+)/) {
+ my $tag = $1;
+ my $x = is_formatted_tag $tag;
+ push @$xtag, $x if $x;
+ }
+ }
+
+ if($xtag) {
+ if($log_entry) {
+ push @{$config->{log}}, $log_entry;
+ last if $opt_max && @{$config->{log}} >= $opt_max;
+ }
+ $log_entry = { commit => $commit, tags => $xtag };
+ }
+ else {
+ $log_entry = { commit => $commit } if !$log_entry;
+ }
+ }
+
+ push @{$log_entry->{lines}}, $_ if $log_entry;
+ }
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# get_log()
+#
+# Read git log and store lines as array in $config->{raw_log} (trailing
+# newlines removed).
+#
+sub get_log
+{
+ chomp(@{$config->{raw_log}} = `git log --pretty=medium --date=raw --topo-order --decorate`);
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# hash_ref = is_formatted_tag(tag_name)
+#
+# Parse tag and return hash ref with branch and version number parts or
+# undef if it doesn't match.
+# - tag_name: tag as string
+# - hash_ref: hash ref with internal tag representation (with keys 'branch' and 'version').
+#
+# This expects tags of the form "VERSION" or "BRANCH-VERSION" where VERSION
+# consists of decimal numbers separated by dots '.' and BRANCH can be any
+# string.
+# (Note: it doesn't really have to be the name of an existing branch.)
+#
+# Tags not conforming to this convention are ignored.
+#
+sub is_formatted_tag
+{
+ if($_[0] =~ /^((.+)-)?((\d+\.)*\d+)$/) {
+ return { branch => $2, version => $3 }
+ }
+
+ return undef;
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# get_branch()
+#
+# Get currently active git branch and store in $config->{branch}.
+#
+# 'master' branch is represented by empty 'branch' key.
+#
+sub get_branch
+{
+ chomp(my $branch = `git rev-parse --abbrev-ref HEAD`);
+
+ $branch = "" if $branch eq 'master';
+
+ $config->{branch} = $branch;
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# res = tag_sort(a, b)
+#
+# Compare 2 tags.
+# - a, b: refs to tag hash
+# - res: -1, 0, 1
+#
+# This is used when we have to decide between alternative tags.
+# (Prefer 'lesser' variant.)
+#
+sub tag_sort
+{
+ my ($x, $y);
+
+ $x = length $a->{version};
+ $y = length $b->{version};
+
+ # longer version number first
+ return $y <=> $x if $y <=> $x;
+
+ return $a->{branch} cmp $b->{branch};
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# str = tag_to_str(tag_ref)
+#
+# Convert tag into string.
+# - tag_ref: ref to hash with 'branch' and 'version' keys
+# - str: string (e.g. "foo-1.44")
+#
+# 'master' branch is represented by missing/empty 'branch' key.
+#
+sub tag_to_str
+{
+ my $tag = $_[0];
+ my $str;
+
+ $str = "$tag->{branch}-" if $tag->{branch} ne "";
+ $str .= $tag->{version};
+
+ return $str;
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# str = tags_to_str(tag_array_ref)
+#
+# Convert array of tags into string.
+# - tag_array_ref: ref to array of tags
+# - str: string (e.g. "(tag1, tag2)"
+#
+# This function is used only internally for debugging.
+#
+sub tags_to_str
+{
+ my $tags = $_[0];
+ my $str;
+
+ for my $t (@$tags) {
+ $str .= ", " if $str;
+ $str .= tag_to_str $t;
+ }
+
+ return "($str)";
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# choose_tags()
+#
+# Scan commit messages and extract tag & branch information.
+#
+# This stores the tag/branch info in $config->{log}[]{tags}.
+#
+sub choose_tags
+{
+ my $branch = $config->{branch};
+
+ for my $x (@{$config->{log}}) {
+ # printf "# %s\n", tags_to_str($x->{tags});
+
+ # no tag info? -> ignore
+ next if !$x->{tags};
+
+ # single tag? -> remember branch info
+ if(@{$x->{tags}} == 1) {
+ $branch = $x->{tags}[0]{branch};
+ next;
+ }
+
+ # several tags? -> choose one
+
+ # any with current branch name?
+ my @t = grep { $_->{branch} eq $branch } @{$x->{tags}};
+
+ # no? -> choose among all
+ @t = @{$x->{tags}} if @t == 0;
+
+ # prefer longest version number, then alphanumerically smallest branch name
+ @t = sort tag_sort @t;
+
+ $branch = $t[0]{branch};
+ $x->{tags} = [ $t[0] ];
+
+ # printf "X %s\n", tags_to_str($x->{tags});
+ }
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# add_head_tag()
+#
+# Suggest tag for HEAD if there isn't one.
+#
+# Basically, use branch + version from most recent tag and increment version.
+#
+sub add_head_tag
+{
+ return if @{$config->{log}} < 2;
+
+ # HEAD tagged already?
+ return if $config->{log}[0]{tags};
+
+ # the first tagged commit if HEAD isn't tagged
+ my $tag = { %{$config->{log}[1]{tags}[0]} };
+
+ # increment version
+ $tag->{version} =~ s/(\d+)$/$1 + 1/e;
+
+ $config->{log}[0]{tags}[0] = $tag;
+
+ # remember that the tag was generated
+ $config->{log}[0]{was_untagged} = 1;
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# fix_dates()
+#
+# Adjust time stamps in entire git log.
+#
+# The time stamps of the git commits are not necessarily ordered by date.
+# But the generated changelog is required to have a strictly monotonic time.
+#
+# We do this by going through the log in reverse and rewriting any dates we
+# find whenever the date decreases.
+#
+# A minimum time difference of 1 second beween entries is maintained.
+#
+# Not very subtle but it works.
+#
+sub fix_dates
+{
+ my $last_date;
+
+ for (reverse @{$config->{raw_log}}) {
+ # e.g. "Date: 1443184889 +0200"
+ if(/^(Date:\s+)(\S+)(\s+\S+)/) {
+ if(defined $last_date && $2 < $last_date) {
+ $_ = "$1$last_date$3\n";
+ }
+ else {
+ $last_date = $2;
+ }
+
+ # ensure a minimal time gap of 1 second
+ $last_date += 1;
+ }
+ }
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# format_all_logs()
+#
+# Format the entire git log.
+#
+# This is done for every code version individually (the log has already been
+# split accordingly).
+#
+# If $config->{start} is set, use this as starting point. Else format the
+# entire git log.
+#
+sub format_all_logs
+{
+ # check if start tag actually exists - if not, print nothing
+ if($config->{start}) {
+ my $tag_found;
+ for (@{$config->{log}}) {
+ $tag_found = 1, last if grep { tag_to_str($config->{start}) eq tag_to_str($_) } @{$_->{tags}};
+ }
+ return if !$tag_found;
+ }
+
+ for (@{$config->{log}}) {
+ if($config->{start}) {
+ # stop if we meet the start tag
+ last if grep { tag_to_str($config->{start}) eq tag_to_str($_) } @{$_->{tags}};
+ }
+ format_log $_;
+ }
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# format_log(log)
+#
+# Format log messages.
+# - log: is an array ref with individual commits
+#
+# All commits belong to a specific code version (stored in $log->{tag}).
+# $log->{formatted} holds the result.
+#
+# The process is done in several individual steps, documented below in the code.
+#
+sub format_log
+{
+ my $log = $_[0];
+
+ my $merge;
+ my $commit;
+ my $saved_commit;
+ my $commits;
+
+ for (@{$log->{lines}}) {
+ if(/^commit (\S+)/) {
+ $commit = { ref => $1 };
+ push @{$commits}, $commit;
+
+ if(
+ $merge &&
+ $merge->{merge_end} eq substr($commit->{ref}, 0, length($merge->{merge_end}))
+ ) {
+ undef $merge;
+ }
+
+ if($merge) {
+ $commit->{merge_ref} = $merge->{ref};
+ $commit->{date} = $merge->{date};
+ # add to all commits so it's not lost when we re-arrange
+ $commit->{merge_msg} = $merge->{msg} if $merge->{msg};
+ # saved entry no longer needed
+ undef $saved_commit;
+ }
+
+ next;
+ }
+
+ if(/^Merge: (\S+)/) {
+ if($commit) {
+ $merge = { merge_end => $1, ref => $commit->{ref} } unless $merge;
+ $saved_commit = pop @{$commits};
+ }
+ undef $commit;
+ next;
+ }
+
+ if(/^Date:\s+(\S.*)/) {
+ if($commit) {
+ $commit->{date} = $1 if !$commit->{date};
+ }
+ elsif($merge) {
+ $merge->{date} = $1 if !$merge->{date};
+ }
+ next;
+ }
+
+ if(/^Author:\s+(\S.*)/) {
+ $commit->{author} = $1 if $commit;
+ $merge->{author} = $1 if $merge && !$merge->{author};
+ next;
+ }
+
+ if($commit) {
+ push @{$commit->{lines}}, $_ if s/^ //;
+ }
+ elsif($merge && !$merge->{msg}) {
+ if(/^ Merge pull request (#\d+) from (\S+)/) {
+ if($config->{github_project}) {
+ $merge->{msg} = "merge gh#$config->{github_project}$1";
+ }
+ else {
+ $merge->{msg} = "merge pr $2";
+ }
+ }
+ elsif(/^ Merge branch '([^']+)'/) {
+ $merge->{msg} = "merge branch $1";
+ }
+ }
+ }
+
+ # it can happen that there's a lonely merge commit left at the end
+ if($merge && $saved_commit) {
+ $saved_commit->{merge_ref} = $merge->{ref};
+ $saved_commit->{date} = $merge->{date};
+ $saved_commit->{author} = $merge->{author};
+ $saved_commit->{merge_msg} = $merge->{msg} if $merge->{msg};
+ $saved_commit->{formatted} = [];
+
+ push @{$commits}, $saved_commit;
+ }
+
+ # Note: the individual steps below work on the array @$commits and modify
+ # its content.
+
+ # step 1
+ # - if there are paragraphs starting with '@log@' or '@+log@'
+ # - delete first paragraph (short summary)
+ # - else
+ # - keep only first paragraph
+ # - if there is a paragraph starting with '@-log', delete entire log
+ # - tag commits that have a '@log@' tag so we can delete untagged commits
+ # belonging to the same merge commit later
+
+ my $tagged_merges = {};
+
+ for my $commit (@$commits) {
+ my $para_cnt = 0;
+ my $delete_all = 0;
+ my $delete_first = 0;
+ for (@{$commit->{lines}}) {
+ $para_cnt++ if $_ eq "";
+ $para_cnt = 0, $delete_first = 1 if /^\@\+log\@/;
+ $delete_all = 1 if /^\@\-log\@/;
+ if(/^\@log\@/) {
+ $para_cnt = 0;
+ $commit->{clear} = 1;
+ $tagged_merges->{$commit->{merge_ref}} = 1 if $commit->{merge_ref} || $log->{was_untagged};
+ }
+ $_ = undef if $para_cnt;
+ }
+ shift @{$commit->{lines}} if $delete_first;
+ $commit->{lines} = [] if $delete_all;
+ }
+
+ # step 2
+ # - clean up tagged commits or commits belonging to tagged merges
+
+ for my $commit (@$commits) {
+ next unless $commit->{clear} || $tagged_merges->{$commit->{merge_ref}};
+ for (@{$commit->{lines}}) {
+ last if /^\@\+?log\@/;
+ $_ = undef;
+ }
+ }
+
+ # step 3
+ # - join lines
+
+ for my $commit (@$commits) {
+ my $lines;
+ my $line;
+
+ for (@{$commit->{lines}}) {
+ next if $_ eq "";
+ if(
+ s/^\s*[+\-][\-\s]*// ||
+ s/^\@\+?log\@// ||
+ $line eq ""
+ ) {
+ s/^\s*//;
+ push @$lines, $line if $line ne "";
+ $line = $_;
+ }
+ else {
+ s/^\s*//;
+ $line .= " " if $line ne "";
+ $line .= $_;
+ }
+ }
+ push @$lines, $line if $line ne "";
+
+ $commit->{formatted} = $lines if $lines;
+ }
+
+ # step 4
+ # - fix small glitches
+
+ for my $commit (@$commits) {
+ next unless $commit->{formatted};
+ for (@{$commit->{formatted}}) {
+ s/(fate|bnc|bsc)\s*(#\d+)/\L$1\E$2/ig;
+ }
+ }
+
+ # step 5
+ # - add merge info at the top or bottom (depending on $opt_merge_msg_before)
+
+ my $merge_logged;
+
+ for my $commit ($opt_merge_msg_before ? reverse(@$commits) : @$commits) {
+ next unless $commit->{formatted};
+
+ if($commit->{merge_ref} && !$merge_logged->{$commit->{merge_ref}}) {
+ $merge_logged->{$commit->{merge_ref}} = 1;
+ if($commit->{merge_msg}) {
+ if($opt_merge_msg_before) {
+ unshift @{$commit->{formatted}}, $commit->{merge_msg};
+ }
+ else {
+ push @{$commit->{formatted}}, $commit->{merge_msg};
+ }
+ }
+ }
+ }
+
+ # step 6
+ # - join commit messages with same author (optionally even with different dates)
+
+ my $commit0;
+
+ for my $commit (@$commits) {
+ next if !$commit->{formatted};
+ $commit0 = $commit, next if !$commit0;
+
+ if(
+ # $commit->{merge_ref} eq $commit0->{merge_ref} &&
+ (
+ $opt_join_author && ($commit->{author} eq $commit0->{author})
+ && (!$opt_keep_date || $commit->{date} eq $commit0->{date})
+ )
+ || $opt_format eq 'internal'
+ ) {
+ unshift @{$commit0->{formatted}}, @{$commit->{formatted}};
+ delete $commit->{formatted};
+ }
+ else {
+ $commit0 = $commit;
+ }
+ }
+
+ # step 7
+ # - add version tag at the end of the first log entry
+
+ for my $commit (@$commits) {
+ next unless $commit->{formatted};
+
+ if($opt_format eq 'obs') {
+ push @{$commit->{formatted}}, $log->{tags}[0]{version} if defined $log->{tags}[0]{version};
+ }
+ else {
+ # push @{$commit->{formatted}}, tag_to_str($log->{tags}[0]);
+ }
+
+ last;
+ }
+
+ # step 8
+ # - add line breaks
+
+ for my $commit (@$commits) {
+ next unless $commit->{formatted};
+ for (@{$commit->{formatted}}) {
+ $_ = add_line_breaks $_;
+ }
+ }
+
+ # step 9
+ # - generate final log message
+ #
+ # note: non-(open)suse email addresses are replaced by $opt_default_email
+
+ my $formated_log;
+
+ for my $commit (@$commits) {
+ next unless $commit->{formatted} && @{$commit->{formatted}};
+
+ if($opt_format eq 'obs') {
+ $formated_log .= "-" x $opt_sep_width . "\n";
+ $formated_log .= format_date_obs($commit->{date});
+ }
+ else {
+ $formated_log .= format_date_iso($commit->{date});
+ }
+ if($opt_format eq 'obs') {
+ my $auth = $commit->{author};
+ $auth =~ s/^.*<//;
+ $auth =~ s/>.*$//;
+ # replace non-suse e-mail addresses with a generic one
+ if($auth !~ /\@(suse\.(com|cz|de)|opensuse\.org)$/) {
+ $auth = $opt_default_email;
+ }
+ $formated_log .= " - $auth\n\n";
+ }
+ else {
+ $formated_log .= ":\t" . tag_to_str($log->{tags}[0]) . "\n";
+ }
+
+ for (@{$commit->{formatted}}) {
+ s/^/\t/mg if $opt_format eq 'internal';
+ $formated_log .= "$_\n";
+ }
+
+ $formated_log .= "\n";
+ }
+
+ $log->{formatted} = $formated_log;
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# new_text = add_line_breaks(text)
+#
+# Add line breaks to text.
+# - text: some text
+# - new_text: same text, reformatted
+#
+# Lines are formatted to have a maximal length of $opt_width. If this causes
+# the last line to be shorter than $opt_width_fuzz, it is appended to the
+# previous line.
+#
+sub add_line_breaks
+{
+ my @words = split /\s+/, @_[0];
+ my $remaining_len = length(join '', @words);
+
+ my $str = shift(@words);
+ my $len = length $str;
+
+ my $next_len;
+ my $word_len;
+
+ for (@words) {
+ $word_len = length;
+ $remaining_len -= $word_len;
+ $next_len = $len + $word_len + 1;
+ if(
+ $next_len >= $opt_width &&
+ $next_len + $remaining_len + 1 >= $opt_width + $opt_width_fuzz
+ ) {
+ $str .= "\n $_";
+ $len = $word_len;
+ }
+ else {
+ $str .= " $_";
+ $len += $word_len + 1;
+ }
+ }
+
+ return "- " . $str;
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# seconds = raw_date_to_s(git_date)
+#
+# Convert git raw date to seconds.
+# - git_date: raw git format (e.g. "1443184889 +0200")
+# - seconds: the seconds part (e.g. "1443184889")
+#
+sub raw_date_to_s
+{
+ return (split / /, $_[0])[0];
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# date = format_date_obs(git_date)
+#
+# Convert git raw date to obs format.
+# - git_date: raw git format (e.g. "1443184889 +0200")
+# - date: obs format ("Fri Sep 25 12:41:29 UTC 2015")
+#
+sub format_date_obs
+{
+ my @d = gmtime(raw_date_to_s($_[0]));
+
+ return
+ qw(Sun Mon Tue Wed Thu Fri Sat)[$d[6]] . " " .
+ qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$d[4]] . " " .
+ $d[3] . " " .
+ sprintf("%02d:%02d:%02d", $d[2], $d[1], $d[0]) . " UTC " .
+ (1900 + $d[5]);
+}
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# date = format_date_iso(git_date)
+#
+# Convert git raw date to iso format.
+# - git_date: raw git format (e.g. "1443184889 +0200")
+# - date: obs format ("2015-09-25")
+#
+sub format_date_iso
+{
+ my @d = gmtime(raw_date_to_s($_[0]));
+
+ return sprintf("%04d-%02d-%02d", 1900 + $d[5], $d[4] + 1, $d[3]);
+}