#!/usr/bin/perl -w # # ucl - update changelog for subversion or git projects (perl version) # # Copyright (c) 2005-2011 by Ricardo Mones # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. # use strict; # project data my $PROJECT = 'clawsker'; # project name my $TYPE = 'git'; # either 'git' or 'svn' my $REPO = 'git@github.com:mones/clawsker.git'; # repository base URL my $USER = "mones"; # project files my $CL = './ChangeLog'; # changelog file my $VF = './VERSION'; # version file my $VC = './VC'; # revision counter # excluded files my @NOTFORRELEASE = ( 'ucl', 'VC' ); # temporary files my $SF = '.ucl.status'; # status file my $PCL = '.ucl.prev'; # previous changelog # global version numbers my $release = undef; sub print_help_info { my $help = <<'ENDOFHELP' Description: ucl Update changelog for Subversion/Git repository Syntax: ucl [options] Where options are: -r x.y.z Update is for release version x.y.z -t x.y.z Tags current trunk as release x.y.z on repository -T x.y.z Makes x.y.z release tarball Notes: Subversion repositories are assumed to have the trunk, branches and tags subtrees just under repository root. Repository tag names are always project-version to ease tarball generation. ENDOFHELP ; print $help; exit 0; } sub repo_tag { my ($tag, $msg) = @_; if ($TYPE eq 'git') { qx/git branch $tag/; } if ($TYPE eq 'svn') { my $sourceurl = "$REPO/trunk/$PROJECT"; my $targeturl = "$REPO/tags/$PROJECT/$tag"; qx/svn cp $sourceurl $targeturl -m $msg/; } } sub repo_get_tag_to_dir { my $tag = shift; # destination dir name is tag too if ($TYPE eq 'git') { qx/git archive --format zip --output .tmp_$tag.zip --prefix=$tag\/ $tag/; qx/unzip .tmp_$tag.zip -d ./; unlink(".tmp_$tag.zip") unless ! -d "$tag"; } if ($TYPE eq 'svn') { my $targeturl = "$REPO/tags/$PROJECT/$tag"; qx/svn export $targeturl/; } } sub repo_update { if ($TYPE eq 'git') { qx/git checkout master/; } if ($TYPE eq 'svn') { qx/LANGUAGE=C svn up/; } } sub repo_get_revision { my $rev = ''; if ($TYPE eq 'git') { open (VCF, "<$VC") or die "opening $VC: $!\n"; $_ = ; chomp; $rev = $_; close (VCF); } if ($TYPE eq 'svn') { $_ = qx/LANGUAGE=C svn info | grep "Revision"/; @_ = split (':'); $rev = $_[1]; $rev =~ s/\s+//; } return $rev; } sub repo_get_modifications { my @mods = (); if ($TYPE eq 'git') { qx/git status --porcelain > $SF/; open (STF, "<$SF") or die "opening $SF: $!\n"; while () { chomp; if (/^\s?M\s+(.*)$/) { push (@mods, "\t* $1"); } if (/^AM?\s+(.*)$/) { push (@mods, "\t* $1\t\t**NEW**"); } if (/^D\s+(.*)$/) { push (@mods, "\t* $1\t\t**REMOVED**"); } if (/^\?\s+(.*)$/) { print "Info: untracked: $1\n"; } } close (STF); unlink ($SF); # remove the status file } if ($TYPE eq 'svn') { qx/svn status > $SF/; open (STF, "<$SF") or die "opening $SF: $!\n"; while () { chomp; if (/^MM?\s+(.*)$/) { push (@mods, "\t* $1"); } if (/^AM?\s+(.*)$/) { push (@mods, "\t* $1\t\t**NEW**"); } if (/^D\s+(.*)$/) { push (@mods, "\t* $1\t\t**REMOVED**"); } if (/^\?\s+(.*)$/) { print "Info: not versioned: $1\n"; } } close (STF); unlink ($SF); # remove the status file } return \@mods; } sub tag_new_release { my $relnum = shift; $_ = $relnum; die "Invalid release version\n" unless (/^\d+\.\d+\.\d+$/); my $namevers = "$PROJECT-$relnum"; my $commitmsg = "'tag release $relnum'"; &repo_tag ($namevers, $commitmsg); } sub tarball_from_release_tag { my $relnum = shift; $_ = $relnum; die "Invalid release version\n" unless (/^\d+\.\d+\.\d+$/); my $namevers = "$PROJECT-$relnum"; &repo_get_tag_to_dir ($namevers); foreach my $file (@NOTFORRELEASE) { if (-f "$namevers/$file") { unlink("$namevers/$file"); } } qx/tar czf $namevers.tar.gz $namevers/; } sub parse_options { if (defined($ARGV[0])) { $_ = $ARGV[0]; if (/-r/) { die "Undefined release version\n" unless defined ($ARGV[1]); $_ = $ARGV[1]; die "Invalid release version\n" unless (/^\d+\.\d+\.\d+$/); $release = $_; } elsif (/-t/) { die "Undefined release version\n" unless defined ($ARGV[1]); &tag_new_release ($ARGV[1]); exit 0; } elsif (/-T/) { die "Undefined release version\n" unless defined ($ARGV[1]); &tarball_from_release_tag ($ARGV[1]); exit 0; } elsif (/-\?/) { &print_help_info (); } else { die "Unknown option. Try -? for help\n"; } } } sub get_changelog_date { my ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime(); $year += 1900; $mon += 1; $hour = ($hour < 10)? "0$hour": "$hour"; $min = ($min < 10)? "0$min": "$min"; $mon = ($mon < 10)? "0$mon": "$mon"; $mday = ($mday < 10)? "0$mday": "$mday"; return "$year-$mon-$mday $hour:$min"; } sub get_version_array { my $rel = shift; if (not defined ($rel)) { # get number from version file open (VTF, "<$VF") or die "opening $VF: $!\n"; $_ = ; chomp; close (VTF); } else { $_ = $rel; } my @vers = (); if (/^(\d+)\.(\d+)\.(\d+)$/) { @vers = ($1, $2, $3, ""); } if (/^(\d+)\.(\d+)\.(\d+)svn(\d+)$/) { @vers = ($1, $2, $3, $4); } if (/^(\d+)\.(\d+)\.(\d+)git(\d+)$/) { @vers = ($1, $2, $3, $4); } return \@vers; } sub get_newer_version { my ($maj, $min, $mic, $ext, $lastr) = @_; my $nvers = "$maj.$min.$mic"; if (not defined($release) or ($release eq "")) { if ($ext ne "") { $nvers = ($nvers . $TYPE . (1 + $lastr)); } } return $nvers; } sub get_newer_header { my $newversion = shift; my $cldate = &get_changelog_date; my $cluser = $USER; # $ENV{USER}; return "$cldate $cluser $newversion"; } sub main { # check we're in the right place -e "$CL" or die "Oops, no ChangeLog here\n"; -e "$VF" or die "Oops, no VERSION here\n"; &parse_options; # parser VERSION file or $release argument my $vs = &get_version_array ($release); my ($major, $minor, $micro, $extra) = (@$vs[0], @$vs[1], @$vs[2], @$vs[3]); # update directory to be sure we're at the last rev &repo_update (); # get last revision from repository my $lastrev = &repo_get_revision (); # calculate modifications my $modifs = &repo_get_modifications (); # save previous changelog rename ($CL, $PCL); # get new version my $newver = &get_newer_version ($major, $minor, $micro, $extra, $lastrev); # write new entry header open (NCL, ">$CL") or die "opening $CL: $!\n"; # write new entry modifications my $newhdr = &get_newer_header ($newver); print NCL "$newhdr\n\n"; foreach my $modif (@$modifs) { print NCL "$modif\n"; } print NCL "\n"; # and previous entries open (PCL, "<$PCL") or die "opening $PCL: $!\n"; while () { print NCL $_; } close (PCL); close (NCL); # and keep a digest for checking my $oldmd5 = qx/md5sum $CL | cut -f1 -d" "/; # edit changelog my $editor = 'vim'; # (defined($ENV{DISPLAY}))? 'gvim -f': 'vim'; if (defined($ENV{EDITOR})) { $editor = $ENV{EDITOR}; } my @editor = ($editor, $CL); system (@editor); # check for changes my $newmd5 = qx/md5sum $CL | cut -f1 -d" "/; if ($oldmd5 eq $newmd5) { print "Unmodified ChangeLog, discarding changes.\n"; unlink ($CL); rename ($PCL, $CL); } else { unlink ($PCL); # update new version after changes qx/echo $newver > $VF/; $TYPE eq 'git' and do { # and counter for git my $newrev = 1 + $lastrev; qx/echo $newrev > $VC/; } } } &main; exit 0;