| Current Path : /home/mudbot/eggdrop/misc/ |
| Current File : //home/mudbot/eggdrop/misc/genchanges |
#! /usr/bin/env tclsh
#
# genchanges - Generate changelog (doc/Changes and ChangeLog) files.
#
# Copyright (C) 2017 - 2019 Eggheads Development Team
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# As a special exception to the GNU General Public License, if you
# distribute this file as part of a program that contains a
# configuration script generated by Autoconf, you may include it under
# the same distribution terms that you use for the rest of that program.
package require Tcl 8.6
package require textutil::adjust
package require base64
# TODO: automatic -i/-e arguments. sort order of git tag --list --sort=v:refname is stable.
# TODO: performance improvements (by reading .git/ directly?)
proc get_usage {} {
return [subst [join {
{Syntax: $::argv0 \[options\] <command>} {} {Commands:}
{short - Generate short changelog (doc/ChangesX.Y)}
{full - Generate full changelog (ChangeLog)}
{release - OVERWRITE ChangeLog and doc/ChangesX.Y (don't use, not done)}
{} {Options (general):}
{-d - Verbose debug logging}
{-r <remote> - Specify remote for tags and public branches}
{-l - Skip fetching from remote. ONLY use this on a fresh clone!}
{} {Options (short):}
{-e <version> - Specify ref to exclude with ancestors}
{-i <version> - Specify ref to include with ancestors (use ./XXX to force local ref XXX, e.g. -i ./release/1.8.2}
{-v <version> - Specify the upcoming version}
{} {Examples:}
{ Generate doc/Changes1.8 for v1.8.2rc3 (exclude v1.8.0 because of the static 1.8.0 blob):}
{ $::argv0 -e v1.6.21 -e v1.8.0 -i v1.8.1 -i v1.8.2 -i stable/1.8 -v 1.8.2rc3 short}
{ Generate ChangeLog for v1.8.3 final:}
{ $::argv0 -i stable/1.8 full}
} \n]]
}
proc commands {} {
lmap cmd [info commands cmd:*] {
string range $cmd 4 end
}
}
proc fatal {msg {showusage 0}} {
if {$msg ne ""} {
puts stderr $msg
if {$showusage} {
puts stderr ""
}
}
if {$showusage} {
puts stderr [get_usage]
}
exit 1
}
proc pop {listVar} {
upvar 1 $listVar list
set e [lindex $list 0]
set list [lrange $list 1 end]
return $e
}
proc dict_lappend {dictVar args} {
upvar 1 $dictVar dict
set add [lindex $args end]
set path [lrange $args 0 end-1]
if {[dict exists $dict {*}$path]} {
set old [dict get $dict {*}$path]
} else {
set old ""
}
dict set dict {*}$path [list {*}$old $add]
}
proc log {text} {
puts stderr $text
}
proc vlog {text} {
if {$::verbose} {
log $text
}
}
proc mustexec {cmd msg} {
vlog "Attempting execute: [join $cmd]"
if {[catch [list exec {*}$cmd] result]} {
fatal "Execution failed. $msg: $result"
}
vlog "Execution successful: $result"
set result [regsub -all -- {\n\n\n+} [string trim $result] "\n\n"]
return $result
}
proc parsecmdline {argv} {
global verbose local
if {![llength $argv]} {
fatal "" 1
}
foreach var {remote command version includes excludes} {
set $var ""
}
set verbose 0
set local 0
while {[llength $argv]} {
set arg [pop argv]
if {[string index $arg 0] eq "-"} {
for {set i 1} {$i < [string length $arg]} {incr i} {
set c [string index $arg $i]
switch -exact -- $c {
"l" { set local 1 }
"r" { set remote [pop argv] }
"d" { set verbose 1 }
"v" { set version [pop argv] }
"i" { lappend includes [pop argv] }
"e" { lappend excludes [pop argv] }
default {
fatal "Unknown option: -$c" 1
}
}
vlog "OptParse: $c (left: $argv)"
}
} else {
set command $arg
break
}
}
if {$command eq ""} {
fatal "No command specified." 1
show_usage
}
if {![llength $includes]} {
fatal "No -i includes specified." 1
}
foreach var {remote version includes excludes} {
cfg$var [set $var]
}
foreach file [textutil::adjust::listPredefined] {
textutil::adjust::readPatterns [textutil::adjust::getPredefined $file]
}
fetchremote
return $command
}
proc indent {text indent} {
textutil::adjust::indent $text [string repeat " " $indent]
}
proc adjust {text {len 120}} {
textutil::adjust::adjust $text -hyphenate true -justify left -length $len -strictlength true
}
interp alias {} cfgincludes {} cfgtags includes
interp alias {} cfgexcludes {} cfgtags excludes
proc cfgtags {varName patterns} {
global remote tags
upvar #0 $varName thesetags
set thesetags ""
if {![info exists tags]} {
set taglist [regexp -all -inline -- {\S+} [mustexec {git tag --list} "Failed getting tag list"]]
set tags ""
foreach tag $taglist {
set commit [string trim [mustexec [list git rev-parse $tag] "Could not rev-parse tag $tag"]]
dict lappend tags $commit $tag
vlog "$tag <- $commit"
}
}
foreach pattern $patterns {
set tmp [dict values $tags $pattern]
if {![llength $tmp]} {
# no matching tags, must be a branch
if {[string range $pattern 0 1] eq "./"} {
# force local branch, illegal branch name
set path [string range $pattern 2 end]
} else {
set path $remote/$pattern
}
mustexec [list git rev-parse --verify -q $path] "Could not find revision '$path'."
set tmp [list $path]
}
lappend thesetags {*}$tmp
}
}
proc cfgversion {version} {
global major
if {$version ne "" && ![regexp {^(\d+\.\d+)\.\d+} $version -> major]} {
fatal "Invalid version number: $version. Try 1.8.1 or 1.8.1rc1 or similar."
}
set ::version $version
}
proc cfgremote {remote} {
set remotes [regexp -all -inline -- {\S+} [exec git remote]]
if {![llength $remotes]} {
fatal "No git remotes configured."
exit 1
}
if {$remote eq ""} {
if {[llength $remotes] == 1} {
set remote [lindex $remotes 0]
} else {
fatal "Multiple remotes available, must specify -r. Available: [join $remotes {, }]"
}
}
if {[llength $remotes] == 1 && $remote eq ""} {
set remote [lindex $remotes 0]
}
if {$remote ni $remotes} {
fatal "Unknown remote: $remote. Available: [join $remotes {, }]"
exit 1
}
vlog "Remotes: '[join $remotes ',']'. Using '$remote'"
set ::remote $remote
}
proc fetchremote {} {
global remote local
if {$local} {
log "Operating locally only, skipping branches/tags-fetch."
return
}
log "Updating tags and branches from remote '$remote'. Branches first..."
mustexec [list -ignorestderr git fetch $remote] "Could not fetch from remote"
log "Branches updated. Fetching tags..."
mustexec [list -ignorestderr git fetch -t $remote] "Could not fetch tags from remote"
log "Tags updated."
}
proc start {} {
global remote
set command [parsecmdline $::argv]
if {$command ni [commands]} {
fatal "Unknown command: $command. Available: [join [commands] {, }]" 1
}
log "Working with remote $remote..."
puts [cmd:$command]
}
proc revlist {excludes includes} {
set includestr $includes
set excludestr [lmap x $excludes { return -level 0 ^$x }]
return [list {*}$includestr {*}$excludestr]
}
proc commitlist {{full 0}} {
global version includes excludes verbose
set commits ""
for {set i 0} {$i < [llength $includes]} {incr i} {
set cmd [list git rev-list --reverse --date-order {*}[revlist [expr {($i == 0 && $full) ? "" : $excludes}] [lrange $includes $i $i]]]
set mycommits [mustexec $cmd "Failed to get revlist"]
foreach commit $mycommits {
if {$commit ni $commits} {
lappend commits $commit
}
}
}
return [lreverse $commits]
}
proc cmd:release {} {
global major version
if {![info exists major] || $version eq ""} {
fatal "Need version number (-v) for short changelog."
}
if {![file exists ChangeLog.gz] || ![file exists doc/Changes$major]} {
fatal "ChangeLog or doc/Changes$major don't exist, are we in the right directory? Then please create them empty if necessary."
}
set short [cmd:short]
set full [cmd:full]
log "Writing ChangeLog.gz"
set fs [open ChangeLog.gz w]
zlib push gzip $fs -level 9
puts $fs [string trim $full \n]
close $fs
log "Exiting, TODO: remove this when shortlog is done!"
exit 0
log "Writing doc/Changes$major"
set fs [open doc/Changes$major w]
puts $fs [string trim $short \n]
close $fs
}
proc clean {data} {
regsub -all -- {(\r)} $data {} data
regsub -all -- {\t} $data { } data
regsub -all -- { +\n} $data "\n" data
regsub -all -- {\n{4,}} $data "\n\n\n" data
return $data
}
proc getcommitinfo:date {commit} {
clock format [getcommitinfo:time $commit] -gmt 1 -format "%Y-%m-%d"
}
proc getcommitinfo:files {commit} {
set data [mustexec [list git show --pretty= --numstat $commit] "Failed to get commit info for $commit"]
set result ""
foreach line [split $data \n] {
if {$line eq ""} {
continue
}
if {![regexp -- {^(\d+|-)\t(\d+|-)\t(.+)$} $line - add del file]} {
error "ERROR ON '$line'"
}
if {$add eq "-" && $del eq "-"} {
lappend result [format "%13s %s" (binary) $file]
} else {
lappend result [format "%6s %6s %s" +$add -$del $file]
}
}
join $result \n
}
proc getcommitinfo:tags {commit} {
global tags
if {![dict exists $tags $commit]} {
return ""
}
dict get $tags $commit
}
proc getcommitinfo:body {commit} {
# roughly where we started using subject/body split messages after cvs->git
if {[getcommitinfo:time $commit] > 1451487300} {
return [getcommitinfofield body $commit]
}
return ""
}
proc getcommitinfo:subject {commit} {
if {[getcommitinfo:time $commit] > 1451487300} {
set msg [string trim [getcommitinfofield subject $commit]]
if {[string index $msg 0] in {"*" "-"}} {
set lines [lmap x [split $msg [string index $msg 0]] { set x [string totitle [string trim $x]]; expr {$x eq "" ? [continue] : "$x"} }]
return [join $lines ". "]
}
return [getcommitinfofield subject $commit]
}
set msg [getcommitinfofield fullbody $commit]
# yes, really (14c25840)
set msg [string map {---------------------------------------------------------------------- ""} $msg]
set lines [lmap l [split $msg \n] { expr {[set l [string trim $l]] eq "" ? [continue] : [string totitle "$l"]} }]
if {![llength $lines]} {
return "*** EMPTY COMMIT MESSAGE ***"
}
return [join $lines ". "]
}
proc getcommitinfo:fullbody {commit} {
set msg [getcommitinfofield fullbody $commit]
set msg [string trim $msg]
if {[string index $msg 0] in {"*" "-"}} {
set lines [lmap x [split $msg [string index $msg 0]] { set x [string trim $x]; expr {$x eq "" ? [continue] : "* $x"} }]
return [join $lines \n]
}
return $msg
}
proc getcommitinfofield {field commit} {
global commitinfocache commitinfofields
# vlog "Getting $commit ($field)"
if {![dict exists $commitinfocache $commit]} {
set result [split [mustexec [list git show -s --pretty=format:[join [dict values $commitinfofields] %x00] $commit] "Failed to get commit info for $commit"] \x00]
for {set i 0} {$i < [dict size $commitinfofields]} {incr i} {
dict set commitinfocache $commit [lindex [dict keys $commitinfofields] $i] [string trim [lindex $result $i]]
}
dict set commitinfocache $commit filelist [split [mustexec [list git show --name-only --format= $commit] "Failed to get commit files for $commit"] \n]
}
regsub -all -- {\r\n?} [dict get $commitinfocache $commit $field] "\n"
}
set commitinfocache ""
set commitinfofields {fullbody %B time %ct authorname %aN authoremail %aE shorthash %h hash %H authordate %aI subject %s body %b}
foreach field [list {*}[dict keys $commitinfofields] filelist] {
if {![llength [info commands getcommitinfo:$field]]} {
interp alias {} getcommitinfo:$field {} getcommitinfofield $field
}
}
proc getcommitinfo {commit args} {
global commitinfocache commitinfofields
set data ""
foreach info $args {
dict set data $info [getcommitinfo:$info $commit]
}
return $data
}
proc reportstatus {what i max} {
if {($i & 0xF) == 0 || $i == $max - 1} {
puts -nonewline stderr "\u001b\[1000D$what ... [format %3d [expr {$i == $max ? 100 : 100*(1+$i)/$max}]] %"
}
if {$i == $max - 1} {
puts ""
}
}
proc cmd:full {} {
global excludes includes tags
set commits [commitlist 1]
set result {""}
for {set i 0} {$i < [llength $commits]} {incr i} {
reportstatus "Generating ChangeLog info" $i [llength $commits]
set commit [lindex $commits $i]
set commitinfo [getcommitinfo $commit body subject authorname authoremail shorthash authordate files]
dict with commitinfo {
set this ""
lappend this "Commit $shorthash ($authordate) by $authorname <$authoremail>" ""
if {[string index $subject 0] in {* -} && [string index $subject 1] eq " "} {
set body [getcommitinfo:fullbody $commit]
} elseif {$subject ne ""} {
lappend this [adjust $subject] ""
}
if {$body ne ""} {
set thisbody ""
foreach line [split $body \n] {
lappend thisbody [indent [adjust $line] 2]
}
lappend this [join $thisbody \n] ""
}
if {$files ne ""} {
lappend this $files ""
}
}
lappend result [join $this \n]
}
log ""
return [clean [join $result \n[string repeat - 120]\n]]
}
proc dateindent {commits date} {
set result ""
for {set i 0} {$i < [llength $commits]} {incr i} {
set msg [lindex $commits $i]
if {$i == 0} {
lappend result "$date $msg"
} else {
lappend result "[string repeat " " [string length $date]] $msg"
}
}
return $result
}
proc versionindent {lines} {
lmap l $lines { return -level 0 " $l" }
}
proc finalformatshortlog {commits} {
set versionresult ""
foreach version [lreverse [dict keys $commits]] {
set versioncommits [dict get $commits $version]
set dateresult ""
foreach date [lreverse [dict keys $versioncommits]] {
set datecommits [dict get $versioncommits $date]
set byresult ""
# force unattributed commits to come first
# if {[dict exists $datecommits ""]} {
# set unattrib [dict get $datecommits ""]
# dict unset datecommits ""
# dict set datecommits "" $unattrib
# }
# foreach by [lreverse [dict keys $datecommits]] {}
foreach datecommit $datecommits {
lassign $datecommit by bycommits
set bycommits [list $bycommits]
# set bycommits [lreverse [dict get $datecommits $by]]
set this [lmap c $bycommits {
set rest [lassign [split [adjust $c 100] \n] first]
set rest [lmap r $rest { return -level 0 " $r" }]
set rest [list "* $first" {*}$rest]
if {$by ne ""} {
lappend rest " \[$by\]"
}
join $rest \n
}]
if {$by ne ""} {
# lappend this " $by"
}
lappend byresult $this
}
vlog "BYRESULT: $byresult"
lappend dateresult "[join [dateindent [split [join [lmap x $byresult { join $x \n }] \n] \n] $date] \n]"
}
vlog "DATERESULT: $dateresult"
lappend versionresult "[expr {$version ne "" ? "Eggdrop $version:" : ""}]\n\n[join [versionindent [split [join $dateresult \n] \n]] \n]"
}
join $versionresult \n\n
}
proc cmd:short {} {
global version verbose tags major
set commits [lreverse [commitlist]]
if {$verbose} {
vlog "--- Start of History ---"
foreach commit $commits {
vlog "* $commit[expr {[dict exists $tags $commit] ? " ([dict get $tags $commit])" : ""}]"
}
vlog "---- End of History ----"
}
foreach key {version date foundby patchby} {
dict set last $key ""
}
set result ""
set thisversion ""
for {set i 0} {$i < [llength $commits]} {incr i} {
reportstatus "Generating doc/Changes info" $i [llength $commits]
set commit [lindex $commits $i]
# skip merge commits unless they have a tag associated to them (e.g. v1.8.1 is a merge commit with a tag)
if {![dict exists $tags $commit] && ![catch {exec git cat-file -t $commit^2}]} {
continue
}
set commitinfo [getcommitinfo $commit fullbody subject date body filelist]
set body [dict get $commitinfo body]
set fullmsg [dict get $commitinfo fullbody]
set shortmsg [dict get $commitinfo subject]
set date [dict get $commitinfo date]
set files [dict get $commitinfo filelist]
set found ""
set patch ""
set newfiles ""
foreach file $files {
set fn [lindex [file split $file] end]
if {$file in {src/patch.h src/version.h ChangeLog.gz ChangeLog} || [string match doc/Changes?.* $file] || $fn in {configure}} {
continue
}
lappend newfiles $file
}
if {![dict exists $tags $commit] && [llength $files] && ![llength $newfiles]} {
vlog "Skipping $commit ($files)"
continue
}
# Subject can contain this information too, scan the whole body, then replace in subject
foreach {- category names} [regexp -nocase -all -inline -- {(found|patch) by:([^\r\n/]+)} $fullmsg] {
foreach nick [split $names {, }] {
set nick [string trim $nick ""]
if {$nick ne ""} {
# dict to deduplicate
dict set [string tolower $category] $nick 1
}
}
}
# Only at the end
regsub -all -nocase -- {(found|patch) by:.*$} $shortmsg {} shortmsg
set by ""
if {[dict size $found]} {
lappend by "Found by: [join [dict keys $found] {, }]"
}
if {[dict size $patch]} {
lappend by "Patch by: [join [dict keys $patch] {, }]"
}
set by [join $by { / }]
# dict_lappend thisversion $date $by $shortmsg
dict_lappend thisversion $date [list $by $shortmsg]
if {[dict exists $tags $commit]} {
# got version tag, everything up to here belongs to that version
dict set result "[dict get $tags $commit] ($date)" $thisversion
set thisversion ""
}
}
log ""
dict set result [expr {$version eq "" ? "" : "v$version"}] $thisversion
set log [finalformatshortlog $result]
return [clean "Eggdrop Changes (Last Updated [clock format [clock seconds] -gmt 1 -format "%Y-%m-%d"]):\n__________________________________________\n\n$log\n\n[zlib decompress [base64::decode $::suffix]]"]
}
# This is the old Changes1.8 file until 1.8.0 which includes manual annotations and changelogs
set suffix {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}
start