Skip to content

Instantly share code, notes, and snippets.

@mtancoigne
Created March 12, 2024 13:38
Show Gist options
  • Save mtancoigne/71b8291c4500385fa7ecc3d5ee390450 to your computer and use it in GitHub Desktop.
Save mtancoigne/71b8291c4500385fa7ecc3d5ee390450 to your computer and use it in GitHub Desktop.
Display commits per branch, day by day
#!/usr/bin/env ruby
# frozen_string_literal: true
# Gems:
# gem install tty-option rugged skittlize activesupport pastel
require 'tty-option'
require 'rugged'
require 'skittlize'
require 'active_support/deprecator' # Required for active_support to work
require 'active_support/deprecation' # Required for active_support to work
require 'active_support/core_ext/integer/time'
require 'pastel'
require 'i18n'
# Ruby standard libs
require 'date'
module ExperimentsLabs
class GitCal
class Error < StandardError; end
def initialize
@pastel = Pastel.new
@repository = Rugged::Repository.discover Dir.pwd
@committer_email = @repository.default_signature[:email]
end
def show_calendar(branches: [], main_branch: nil, days: nil)
raise 'You should provide an array of branches' unless branches.is_a? Array
days = days ? days.to_i : nil
@branches = process_branches_args branches
@main_branch = main_branch
today = Time.new.beginning_of_day
@start_date = days ? (today - days.days) : Time.new.beginning_of_month
@days_to_show = days || today.day
list
render
end
private
def list # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@commits_per_day = {}
puts "Commits by #{@committer_email}, from #{@start_date.to_date} to now"
@branches.each do |branch, branch_commit_id|
commit_list = commits_for branch_commit_id
@days_to_show.times do |index|
day = (@start_date + index.days).to_date
day_string = day.to_s
@commits_per_day[day_string] ||= {}
commit_list.select { |commit| day == commit.committer[:time].to_date }
.each do |commit|
message = commit.message.split("\n").first
@commits_per_day[day_string][message] ||= []
@commits_per_day[day_string][message].push branch
end
end
end
end
def commits_for(head_hash)
walker = Rugged::Walker.new @repository
walker.sorting Rugged::SORT_DATE
walker.push head_hash
walker.select do |commit|
commit.author[:time].after?(@start_date) && commit.author[:email] == @committer_email
end
end
def render # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
# Rendering
@commits_per_day.each do |day, commits|
next if commits.empty?
puts @pastel.bold Date.parse(day).strftime('%v (%a)')
commits.keys.sort.each do |message|
branches_list = if @branches.size > 1
commits[message]
# Ignore other branches when a commit is already on the main branch
.reject { |branch| @main_branch && commits[message].include?(@main_branch) && branch != @main_branch }
.map(&:skittlize)
.join(', ')
end
branches_list = branches_list ? "[#{branches_list}] " : ''
if message.match?(/^merge /i)
message = @pastel.decorate(message, :dim, :strikethrough)
else
nibbles = message.split(':')
nibbles[0] = nibbles[0].skittlize if nibbles.size > 1
message = nibbles.join(':')
end
puts " #{branches_list}#{message}"
end
puts "\n"
end
end
# Checks branches argument against repository branches and returns a hash
# with the git object id
#
# @return [Hash] - Like <branch>: <branch_commit_id>
def process_branches_args(branches)
return { @repository.head.name.sub(%r{^refs/heads/}, '').to_s => @repository.head.target_id } if branches.empty?
branches = {}
branches.each do |name|
branch = repository.branches[name]
raise GitCal::Error, "Invalid branch #{name}" unless branch
branches[name] = branch.target_id
end
branches
end
end
class GitCalCli
include TTY::Option
usage do
program 'git-cal'
desc 'Display commits per branch, day by day'
no_command
example <<~TXT
git cal # list commits of the current branch
git cal branch1, branch2 # list commits for the given branches
TXT
end
argument :branches do
optional
arity zero_or_more
default []
desc 'Branch to use in listing'
end
option :main do
optional
long '--main string'
short '-m'
desc 'Main branch: commits present in this branch will be hidden from others'
end
option :days do
optional
long '--days int'
short '-d'
desc 'Optional amount of days to use, instead of the beginning of month'
validate(/^\d+$/)
end
flag :help do
short '-h'
long '--help'
desc 'Print usage'
end
def run
handle_help
cal = GitCal.new
cal.show_calendar branches: params[:branches],
main_branch: params[:main],
days: params[:days]
rescue GitCal::Error => e
puts e
exit 1
end
private
def handle_help
return unless params[:help]
print help
exit 0
end
end
end
cli = ExperimentsLabs::GitCalCli.new
cli.parse
cli.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment