3 # echangelog: Update the ChangeLog for an ebuild. For example:
5 # $ echangelog 'Add ~alpha to KEYWORDS'
7 # > 10 Feb 2003; Aron Griffis <agriffis@gentoo.org> oaf-0.6.8-r1.ebuild :
8 # > Add ~alpha to KEYWORDS
12 use POSIX qw(strftime getcwd setlocale);
17 # Fix bug 21022 by restricting to C locale
18 setlocale(&POSIX::LC_ALL, "C");
21 $Text::Wrap::columns = 77;
22 $Text::Wrap::unexpand = 0;
25 my (@files, @ebuilds, @conflicts, @trivial, @unknown, @new_versions, %actions);
26 my ($input, $editor, $entry, $user, $date, $text, $year, $vcs);
27 my ($opt_help, $opt_strict, $opt_version);
35 diff => "cvs -f diff -U0",
36 status => "cvs -fn up",
39 regex => qr/^Index: (([^\/]*?)\.ebuild)\s*$/
42 diff => "svn diff -N",
43 status => "svn status",
46 regex => qr/^Index: (([^\/]*?)\.ebuild)\s*$/
50 status => "git diff-index HEAD --name-status",
52 # This value should usually be 3 but on new file mode we need skip+1.
53 # So 4 should be fine anyway.
55 regex => qr/^diff \-\-git \S*\/((\S*)\.ebuild)/
59 status => "hg status .",
62 # hg diff is relative to the root.
63 # TODO: Write a proper regex :)
64 regex => qr/diff \-r \S+ \S+\/\S+\/((\S+)\.ebuild)/
69 (my $usage = <<" EOF") =~ s/^\t//gm;
70 Usage: echangelog [options] <changelog message>
73 --help err, this screen ...
74 --strict abort on trivial/no changes
75 --version show version info
82 my $Revision = "Last svn change rev";
83 my $Date = "Last svn change date";
85 print "echangelog\n$Revision$foo \n$Date$foo\n";
92 # Ensure our variable exist
93 if ( defined($ENV{$key}) ) {
94 # Ensure we don't get empty variables
95 if ( length($ENV{$key}) > 0 ) {
103 # Copied from Text::Wrap.
104 # The only modified thing is:
105 # We trim _just_ tab/space etc. but not \n/\r.
106 # \s treats even \n/\r as whitespace.
114 # See 'my $ps = ($ip eq $xp) ? "\n\n" : "\n";'
116 my ($ip, $xp, @raw) = @_;
120 for $pp ( split(/\n\s+/, join("\n", @raw)) ) {
121 $pp =~ s/[\x09|\x0B|\x0C|\x20]+/ /g;
122 my $x = Text::Wrap::wrap($ip, $xp, $pp);
126 # if paragraph_indent is the same as line_indent,
127 # separate paragraphs with blank lines
128 my $ps = ($ip eq $xp) ? "\n\n" : "\n";
129 return join ($ps, @para);
132 sub changelog_info(%) {
135 open(INFO, '>', 'ChangeLog.new');
138 print(INFO "# Please enter the ChangeLog message for your changes. Lines starting\n");
139 print(INFO "# with '#' will be ignored, and an empty message aborts the ChangeLog.\n");
140 print(INFO "#\n# Changes:\n");
142 foreach my $key (keys(%changed)) {
143 if ($changed{$key} eq "+") {
144 printf(INFO "# new file:\t%s\n", $key);
146 elsif ($changed{$key} eq "-") {
147 printf(INFO "# deleted:\t%s\n", $key);
150 printf(INFO "# modified:\t%s\n", $key);
161 my $category = basename(dirname($cwd));
162 my $package_name = basename($cwd);
164 $t =~ s/^(# ChangeLog for).*/$1 $category\/$package_name/;
170 'help' => \$opt_help,
171 'strict' => \$opt_strict,
172 'version' => \$opt_version,
175 usage() if $opt_help;
176 version() if $opt_version;
178 # Figure out what kind of repo we are in.
181 } elsif ( -d '.svn' ) {
184 # Respect $PATH while looking for git
185 if (getenv("PATH")) {
186 foreach my $path ( split(":", getenv("PATH")) ) {
187 if ( -X "$path/git" ) {
188 open(GIT, '-|', "git rev-parse --git-dir 2>/dev/null");
189 $vcs = "git" if defined(<GIT>);
193 if ( -X "$path/hg" ) {
194 open(HG, '-|', "hg root 2>/dev/null");
195 $vcs = "hg" if defined(<HG>);
203 die "No CVS, .git, .svn directories found, what kind of repo is this?";
207 # Read the current ChangeLog
208 if (-f 'ChangeLog') {
209 open(I, '<', 'ChangeLog') or die "Can't open ChangeLog for input: $!\n";
210 { local $/ = undef; $text = <I>; }
213 # No ChangeLog here, maybe we should make one...
215 open(C, '-|', "portageq portdir") or die "portageq returned with an error: $!\n";
217 $portdir =~ s/\s+$//;
220 die "Can't find PORTDIR\n" if (length $portdir == 0);
222 open(I, '<', "$portdir/skel.ChangeLog")
223 or die "Can't open $portdir/skel.ChangeLog for input: $!\n";
224 { local $/ = undef; $text = <I>; }
227 $text =~ s/^\*.*//ms; # don't need the fake entry
229 $text = update_cat_pn($text);
231 die "This should be run in a directory with ebuilds...\n";
235 # Figure out what has changed around here
236 open C, $vcs{$vcs}{status}.' 2>&1 |' or die "Can't run ".$vcs{$vcs}{status}.": $!\n";
241 $filename =~ /\S*\/(\S*)/;
253 } elsif (/^\?\s+(\S+)/) {
256 $filename =~ /\S*\/(\S*)/;
270 } elsif (/^([ARMD])\s+\+?\s*(\S+)/) {
271 my ($status, $filename) = ($1,$2);
274 open P, "git rev-parse --sq --show-prefix |";
276 $prefix = substr($prefix, 0, -1);
279 if ($filename =~ /$prefix(\S*)/) {
291 push @files, $filename;
292 ($actions{$filename} = $status) =~ tr/DARM/-+-/d;
296 # git only shows files already added so we need to check for unknown files
299 find(\&git_unknown_objects, "./");
302 sub git_unknown_objects {
304 my ($dev,$ino,$mode,$nlink,$uid,$gid);
306 # Ignore empty directories - git doesn't version them and cvs removes them.
307 if ( (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && ! -d _ ) {
308 open C, $vcs." status $_ 2>&1 1>/dev/null |";
312 push @unknown, $object;
319 # Separate out the trivial files for now
321 !/files.digest|Manifest|ChangeLog/ or do { push @trivial, $_; 0; }
325 !/files.digest|Manifest|ChangeLog/ or do { push @trivial, $_; 0; }
328 # Don't allow any conflicts
331 $vcs reports the following conflicts. Please resolve them before
334 print STDERR map "C $_\n", @conflicts;
338 # Don't allow unknown files (other than the trivial files that were separated
342 $vcs reports the following unknown files. Please use "$vcs add" before
343 running echangelog, or remove the files in question.
345 print STDERR map "? $_\n", @unknown;
349 # Sort the list of files as portage does. None of the operations through
350 # the rest of the script should break this sort.
353 (my $va = $a) =~ s/.*?-(\d.*?)(?:\.ebuild)?$/$1/;
354 (my $vb = $b) =~ s/.*?-(\d.*?)(?:\.ebuild)?$/$1/;
355 my ($na, $sa, $sna, $ra) = ($va =~ /^(.*?)(?:_(alpha|beta||pre|rc|p)(\d*))?(?:-r(\d+))?$/);
356 my ($nb, $sb, $snb, $rb) = ($vb =~ /^(.*?)(?:_(alpha|beta||pre|rc|p)(\d*))?(?:-r(\d+))?$/);
357 my (@na) = split /\.|(?<=\d)(?=[^\d\.])/, $na;
358 my (@nb) = split /\.|(?<=\d)(?=[^\d\.])/, $nb;
362 # compare version numbers first
364 for (my $i = 0; defined $na[$i] or defined $nb[$i]; $i++) {
366 return +1 if defined $na[$i] and !defined $nb[$i];
367 return -1 if defined $nb[$i] and !defined $na[$i];
370 if ($na[$i] =~ /^\d/ and $nb[$i] =~ /^\d/) {
371 $retval = ($na[$i] <=> $nb[$i]);
372 return $retval if $retval;
377 if ($na[$i] =~ /^\D/ and $nb[$i] =~ /^\D/) {
378 $retval = ($na[$i] cmp $nb[$i]);
379 return $retval if $retval;
384 $retval = ($na[$i] =~ /\d/ and -1 or +1);
389 # compare suffix second
391 if (defined $sa and !defined $sb) {
392 return +2 if $sa eq "p";
395 if (defined $sb and !defined $sa) {
396 return -3 if $sb eq "p";
400 if (defined $sa) { # and defined $sb
401 $retval = ($sa cmp $sb);
403 return +4 if $sa eq "p";
404 return -4 if $sb eq "p";
405 return $retval; # suffixes happen to be alphabetical order, mostly
408 # compare suffix number
409 return +5 if defined $sna and !defined $snb;
410 return -5 if defined $snb and !defined $sna;
412 if (defined $sna) { # and defined $snb
413 $retval = ($sna <=> $snb);
414 return $retval if $retval;
421 return +6 if defined $ra and !defined $rb;
422 return -6 if defined $rb and !defined $ra;
424 if (defined $ra) { # and defined $rb
425 return ($ra <=> $rb);
429 # nothing left to compare
434 # Just to ensure we don't get duplicate entries.
438 foreach my $value (@_) {
439 push(@{$aref}, $value) if !grep(/^$value$/, @{$aref});
443 # Forget ebuilds that only have changed copyrights, unless that's all
444 # the changed files we have
445 @ebuilds = grep /\.ebuild$/, @files;
446 @files = grep !/\.ebuild$/, @files;
450 open C, $vcs{$vcs}{diff}." HEAD -- @ebuilds 2>&1 |" or die "Can't run: ".$vcs{$vcs}{diff}."$!\n";
452 open C, $vcs{$vcs}{diff}." @ebuilds 2>&1 |" or die "Can't run: ".$vcs{$vcs}{diff}."$!\n";
458 # only possible with cvs
459 if (/^$vcs diff: (([^\/]*?)\.ebuild) was removed/) {
462 # We assume GNU diff output format here.
463 # git format: diff --git a/app-doc/repodoc/metadata.xml b/app-doc/repodoc/metadata.xml
464 elsif (/$vcs{$vcs}{regex}/) {
471 last if /^deleted file mode|^index/;
472 if (/^new file mode/) {
474 mypush(@new_versions, $version);
480 # check if more than just copyright date changed.
481 # skip some lines (vcs dependent)
482 foreach(1..$vcs{$vcs}{skip}) {
488 if (/^[-+](?!# Copyright)/) {
494 # at this point we've either added $f to @files or not,
495 # and we have the next line in $_ for processing
498 elsif (/^$vcs.*?: (([^\/]*?)\.ebuild) is a new entry/) {
500 mypush(@new_versions, $2);
503 # other cvs output is ignored
509 # Subversion diff doesn't identify new versions. So use the status command
510 if (($vcs eq "svn") and (@ebuilds)) {
511 open C, $vcs{$vcs}{status}." @ebuilds 2>&1 |" or die "Can't run: ".$vcs{$vcs}{status}."$!\n";
515 if (/^A\s+\+?\s*(([^\s]*)\.ebuild)/) {
517 mypush(@new_versions, $2);
524 # When a package move occurs, the versions appear to be new even though they are
525 # not. Trim them from @new_versions in that case.
526 @new_versions = grep { $text !~ /^\*\Q$_\E\s/m } @new_versions;
528 # Check if we have any files left, otherwise re-insert ebuild list
529 # (of course, both might be empty anyway)
530 @files = @ebuilds unless (@files);
532 # Allow ChangeLog entries with no changed files, but give a fat warning
535 print STDERR "** NOTE: No non-trivial changed files found. Normally echangelog\n";
536 print STDERR "** should be run after all affected files have been added and/or\n";
537 print STDERR "** modified. Did you forget to $vcs add?\n";
541 print STDERR "** In strict mode, exiting\n";
545 @files = sort sortfunc @trivial;
547 # last resort to put something in the list
549 @files = qw/ChangeLog/;
550 $actions{'ChangeLog'} = "";
555 @files = sort sortfunc @files;
556 @new_versions = sort sortfunc @new_versions;
558 # Get the input from the cmdline, editor or stdin
562 $editor = getenv('ECHANGELOG_EDITOR') ? getenv('ECHANGELOG_EDITOR') : getenv('EDITOR') || undef;
565 # Append some informations.
566 changelog_info(%actions);
568 system("$editor ChangeLog.new");
571 # This usually happens when the editor got forcefully killed; and
572 # the terminal is probably messed up: so we reset things.
574 print STDERR "Editor died! Reverting to stdin method.\n";
577 if (open I, "<ChangeLog.new") {
582 # Remove comments from changelog_info().
584 $input =~ s/^#.*//mg;
587 print STDERR "Error opening ChangeLog.new: $!\n";
588 print STDERR "Reverting to stdin method.\n";
592 unlink('ChangeLog.new') if -f 'ChangeLog.new';
596 print "Please type the log entry: use Ctrl-d to finish, Ctrl-c to abort...\n";
601 die "Empty entry; aborting\n" unless $input =~ /\S/;
603 # If there are any long lines, then wrap the input at $columns chars
604 # (leaving 2 chars on left, one char on right, after adding indentation below).
605 $input = text_fill(' ', ' ', $input);
607 # Prepend the user info to the input
608 # Changes related to bug 213374;
609 # This sequence should be right:
610 # 1. GENTOO_COMMITTER_NAME && GENTOO_COMMITTER_EMAIL
611 # 2. GENTOO_AUTHOR_NAME && GENTOO_AUTHOR_EMAIL
612 # 3. ECHANGELOG_USER (fallback/obsolete?)
614 if ( getenv("GENTOO_COMMITTER_NAME") && getenv("GENTOO_COMMITTER_EMAIL") ) {
615 $user = sprintf("%s <%s>", getenv("GENTOO_COMMITTER_NAME"), getenv("GENTOO_COMMITTER_EMAIL"));
617 elsif ( getenv("GENTOO_AUTHOR_NAME") && getenv("GENTOO_AUTHOR_EMAIL") ) {
618 $user = sprintf("%s <%s>", getenv("GENTOO_AUTHOR_NAME"), getenv("GENTOO_AUTHOR_EMAIL"));
620 elsif ( getenv("ECHANGELOG_USER") ) {
621 $user = getenv("ECHANGELOG_USER");
624 my ($fullname, $username) = (getpwuid($<))[6,0];
625 $fullname =~ s/,.*//; # remove GECOS, bug 80011
626 $user = sprintf('%s <%s@gentoo.org>', $fullname, $username);
629 # Make sure that we didn't get "root"
630 die "Please set ECHANGELOG_USER or run as non-root\n" if $user =~ /<root@/;
632 $date = strftime("%d %b %Y", gmtime);
633 $entry = "$date; $user ";
634 $entry .= join ', ', map "$actions{$_}$_", @files;
636 $entry = Text::Wrap::fill(' ', ' ', $entry); # does not append a \n
637 $entry .= "\n$input"; # append user input
639 # Each one of these regular expressions will eat the whitespace
640 # leading up to the next entry (except the two-space leader on the
641 # front of a dated entry), so it needs to be replaced with a
642 # double carriage-return. This helps to normalize the spacing in
645 # Insert at the top with a new version marker
646 $text =~ s/^( .*? ) # grab header
647 \s*\n(?=\ \ \d|\*|\z) # suck up trailing whitespace
649 join("\n", map "*$_ ($date)", reverse @new_versions) .
651 or die "Failed to insert new entry (4)\n";
653 # Changing an existing patch or ebuild, no new version marker
655 $text =~ s/^( .*? ) # grab header
656 \s*\n(?=\ \ \d|\*|\z) # suck up trailing whitespace
658 or die "Failed to insert new entry (3)\n";
661 # New packages and/or ones that have moved around often have stale data here.
662 # But only do that in places where ebuilds are around (as echangelog can be
663 # used in profiles/ and such places).
664 if (grep(/\.ebuild$/, @files)) {
665 $text = update_cat_pn($text);
668 sub update_copyright {
670 (my $year = $date) =~ s/.* //;
672 $t =~ s/^# Copyright \d+(?= )/$&-$year/m or
673 $t =~ s/^(# Copyright) \d+-(\d+)/$1 1999-$year/m;
678 # Update the copyright year in the ChangeLog
679 $text = update_copyright($text);
681 # Write the new ChangeLog
682 open O, '>ChangeLog.new' or die "Can't open ChangeLog.new for output: $!\n";
683 print O $text or die "Can't write ChangeLog.new: $!\n";
684 close O or die "Can't close ChangeLog.new: $!\n";
686 # Update affected ebuild copyright dates. There is no reason to update the
687 # copyright lines on ebuilds that haven't changed. I verified this with an IP
689 for my $e (grep /\.ebuild$/, @files) {
691 my ($etext, $netext);
693 open E, "<$e" or warn("Can't read $e to update copyright year\n"), next;
694 { local $/ = undef; $etext = <E>; }
697 # Attempt the substitution and compare
698 $netext = update_copyright($etext);
699 next if $netext eq $etext; # skip this file if no change.
701 # Write the new ebuild
702 open E, ">$e.new" or warn("Can't open $e.new\n"), next;
704 close E or warn("Can't write $e.new\n"), next;
706 # Move things around and show the diff
707 system "diff -U 0 $e $e.new";
708 rename "$e.new", $e or warn("Can't rename $e.new: $!\n");
710 # git requires to re-add this file else it wouln't be included in the commit.
713 system("$vcs{$vcs}{add} ${e}");
718 # Move things around and show the ChangeLog diff
719 system 'diff -Nu ChangeLog ChangeLog.new';
720 rename 'ChangeLog.new', 'ChangeLog' or die "Can't rename ChangeLog.new: $!\n";
722 # Okay, now we have a starter ChangeLog to work with.
723 # The text will be added just like with any other ChangeLog below.
724 # Add the new ChangeLog to vcs before continuing.
726 if (open F, "CVS/Entries") {
727 system("cvs -f add ChangeLog") unless (scalar grep /^\/ChangeLog\//, <F>);
729 } elsif ($vcs eq "svn") {
730 if (open F, ".svn/entries") {
731 system("svn add ChangeLog") unless (scalar grep /ChangeLog/, <F>);
734 system("$vcs{$vcs}{add} ChangeLog 2>&1 >> /dev/null");
737 # vim: set ts=4 sw=4 tw=0: