File: genchanges

package info (click to toggle)
eggdrop 1.10.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 9,596 kB
  • sloc: ansic: 65,863; javascript: 8,908; sh: 5,337; tcl: 3,801; makefile: 1,771; python: 121
file content (603 lines) | stat: -rwxr-xr-x 18,793 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
#! /usr/bin/env tclsh
#
# genchanges - Generate changelog (doc/Changes and ChangeLog) files.
#
# Copyright (C) 2017 - 2025 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 {
			dict set commits $commit 1
		}
	}
	# exclude cherry-picked commits, assumes correct order, WIP, assumes chronic order of -i
	# --cherry-pick is only relevant when using symmetric difference which is A...B
	# the above method of gather commits by manually grabbing all EXCLUDE..INCLUDE does not use it
	for {set i 0} {$i < [llength $includes] - 1} {incr i} {
		for {set j [expr {1+$i}]} {$j < [llength $includes]} {incr j} {
			set i1 [lindex $includes $i]
			set i2 [lindex $includes $j]
			set cmd [list git rev-list --cherry-mark --no-merges --right-only --reverse --date-order $i1...$i2]
			set mycommits [mustexec $cmd "Failed to get revlist"]
			foreach commit $mycommits {
				if {[string index $commit 0] eq "=" && [dict exists $commits [string range $commit 1 end]]} {
					dict unset commits [string range $commit 1 end]
				}
			}
		}
	}
	return [lreverse [dict keys $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"]
}


start