Last active
December 30, 2015 21:09
-
-
Save sycobuny/7885917 to your computer and use it in GitHub Desktop.
Do annotated tags automatically based on branches, for doing a rolling upgrade from puppet 2.x to puppet 3.x.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[alias] | |
root = rev-parse --show-toplevel | |
current-branch = rev-parse --abbrev-ref HEAD | |
ls-untracked = ls-files -o --exclude-standard | |
release = !"$(git root)/.git/local-commands/release.pl" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl | |
use warnings; | |
use strict; | |
predeclare_subs: { | |
sub prefix(); # the tag prefix for releases on this copy of the repo | |
sub name(); # the name of this copy of the repo | |
sub getlines(); # all lines fed into this script | |
sub gettags(); # all tag (#s), based on prefix, fed into this script | |
sub latest(); # the latest tag this script was given | |
sub earliest(); # the earliest tag this script should care about | |
sub showrev($); # show a revision for a tag (#) | |
sub line(); # a VERY pretty line for separating the email sections | |
sub from_name(); # the originating user name for the email | |
sub from_user(); # the user part of the originating email address | |
sub from_host(); # the host part of the originating email address | |
sub from_email(); # the originating address for the email | |
sub from(); # the full originating email address | |
sub subject(); # the subject for this message | |
sub recipients(); # the addresses of people to email | |
sub body(); # the full body of the email to send | |
sub sendmail(); # send the email out to users | |
sub __main__(); # run the full script | |
} | |
prefix_sub: { | |
my ($beenrun, $prefix); | |
sub prefix() { | |
return $prefix if $beenrun; | |
$beenrun = 1; | |
$prefix = `git config release.prefix`; | |
chomp($prefix); | |
prefix; | |
} | |
} | |
name_sub: { | |
my ($beenrun, $name); | |
sub name() { | |
return $name if $beenrun; | |
$beenrun = 1; | |
$name = `git description`; | |
chomp($name); | |
name; | |
} | |
} | |
getlines_sub: { | |
my ($returnlines, $beenrun, @lines); | |
$returnlines = sub { wantarray ? @lines : join("\n", @lines) }; | |
sub getlines() { | |
return $returnlines->() if $beenrun; | |
$beenrun = 1; | |
foreach my $line (<>) { | |
chomp($line); | |
push(@lines, $line); | |
} | |
getlines; | |
} | |
} | |
gettags_sub: { | |
my ($beenrun, @tags); | |
sub gettags() { | |
return @tags if $beenrun; | |
$beenrun = 1; | |
my ($prefix) = prefix; | |
foreach my $line (getlines) { | |
my ($old, $new, $spec) = split(/ /, $line); | |
chomp($spec); | |
next unless ($old =~ /^0+$/); | |
next unless ($spec =~ m{^refs/tags/$prefix-([0-9]{1,})$}); | |
push(@tags, $1); | |
} | |
# sort the tags and stick one on the beginning from just before the | |
# push, so we can construct a complete history of this release. | |
@tags = sort @tags; | |
unshift(@tags, earliest) if @tags; | |
gettags; | |
} | |
} | |
latest_sub: { | |
sub latest() { | |
my (@tags) = gettags; | |
$tags[$#tags]; | |
} | |
} | |
earliest_sub: { | |
my ($beenrun, $earliest); | |
sub earliest() { | |
return $earliest if $beenrun; | |
$beenrun = 1; | |
my (@tags) = gettags; | |
my ($prefix) = prefix; | |
my ($first) = $tags[0]; | |
my ($dashes) = scalar($prefix =~ /-/g) + 2; | |
my ($command) = join(' | ', ( | |
'git tag', | |
"grep -e '^$prefix-[0-9]\\{1,\\}\$'", | |
"cut -d- -f$dashes", | |
'sort -rn', | |
"grep -A 1 -e '$first\$'", | |
'tail -n1', | |
)); | |
my ($before) = `$command`; | |
chomp($before); | |
$earliest = ($before == $first) ? '' : $before; | |
} | |
} | |
showrev_sub: { | |
sub showrev($) { | |
my ($rev) = `git rev-parse @{[ prefix ]}-@{[ shift ]}`; | |
chomp($rev); | |
$rev; | |
} | |
} | |
line_sub: { | |
my ($line) = "\n@{[ '=*' x 35 ]}=\n"; | |
sub line() { $line } | |
} | |
from_subs: { | |
my ($name, $user, $host, $email, $from); | |
sub from_name() { | |
return $name if $name; | |
$name = `git config release.from.name`; | |
chomp($name); | |
$name ||= 'Git Release Manager'; | |
} | |
sub from_user() { | |
return $user if $user; | |
$user = `git config release.from.user`; | |
chomp($user); | |
$user ||= 'git'; | |
} | |
sub from_host() { | |
return $host if $host; | |
my ($uname); | |
$host = `git config release.from.host`; | |
chomp($host); | |
unless ($host) { | |
$uname = `uname -a`; | |
if ($uname =~ /Darwin|FreeBSD|Ubuntu/) { $host = `hostname` } | |
else { $host = `hostname --fqdn` } | |
chomp($host); | |
} | |
from_host; | |
} | |
sub from_email() { | |
return $email if $email; | |
$email = `git config release.from.email`; | |
chomp($email); | |
$email ||= "@{[ from_user ]}@@{[ from_host ]}"; | |
} | |
sub from() { | |
return $from if $from; | |
$from = qq{"@{[ from_name ]}" <@{[ from_email ]}>}; | |
} | |
} | |
subject_sub: { | |
my ($subject); | |
sub subject() { | |
return $subject if $subject; | |
$subject = `git config release.subject`; | |
chomp($subject); | |
$subject =~ s/%s/name/eg; | |
$subject ||= "New Tag-Release for @{[ name ]}: @{[ latest ]}"; | |
} | |
} | |
recipients_sub: { | |
my ($beenrun, $recipients); | |
sub recipients() { | |
return $recipients if $beenrun; | |
$beenrun = 1; | |
local ($_); | |
my (@recipients) = `git config --get-all release.recipient`; | |
$recipients = join(', ', map { chomp; $_ } @recipients); | |
recipients | |
} | |
} | |
body_sub: { | |
my ($body); | |
sub body() { | |
return $body if $body; | |
my ($prefix) = prefix; | |
my ($earliest) = earliest; | |
my ($latest) = latest; | |
my (@tags) = gettags; | |
my ($shortlog); | |
$body = "A new release has been issued for @{[ name ]}!\n"; | |
if ($earliest) { | |
$body .= "This release covers from tag $prefix-$earliest to " . | |
"$prefix-$latest.\n\n"; | |
$shortlog = `git shortlog $prefix-$earliest..$prefix-$latest`; | |
} | |
else { | |
$body .= "This release covers all history until " . | |
"$prefix-$latest.\n\n"; | |
$shortlog = `git shortlog $prefix-$latest`; | |
} | |
$body .= "All commits covered in this release:\n"; | |
$body .= $shortlog . line; | |
do { | |
my ($from, $to, $log, $header, $command) = ($tags[0], $tags[1]); | |
shift @tags; | |
if ($from) { | |
$log = `git log --stat $prefix-$from..$prefix-$to`; | |
$header = "Log from $prefix-$from to $prefix-$to:\n"; | |
$command = "(to view all changes run `git log --patch " . | |
"$prefix-$from..$prefix-$to` locally)\n"; | |
} | |
else { | |
$log = `git log --stat $prefix-$to`; | |
$header = "Log up to $prefix-$to:\n"; | |
$command = "(to view all changes run `git log --patch " . | |
"$prefix-$to` locally)\n"; | |
} | |
$body .= $header . $command . $log . line; | |
} while (scalar(@tags) > 2); | |
$body .= "\n\nThank you,\n"; | |
$body .= from_name; | |
$body .= "\n\n-----\n(This email is auto-generated; do not reply)"; | |
} | |
} | |
sendmail_sub: { | |
sub sendmail() { | |
my (@command) = (qw(sendmail -t -f), from); | |
my (%headers) = ( | |
# regular envelope headers | |
From => from, | |
To => recipients, | |
Subject => subject, | |
# standard-ish git headers from built-in git emails | |
'X-Git-Refname' => "@{[ prefix ]}-@{[ latest ]}", | |
'X-Git-Oldrev' => showrev(earliest), | |
'X-Git-Newrev' => showrev(latest), | |
'X-Git-Reftype' => 'tag', | |
); | |
my (@headers); | |
while (my ($k, $v) = each(%headers)) { | |
push(@headers, "$k: $v"); | |
} | |
print join("\n", @headers) . "\n\n" . body . "\n"; | |
} | |
} | |
main_sub: { | |
sub __main__() { | |
sendmail if gettags; | |
exit 0; | |
} | |
} | |
__main__ if $0 eq __FILE__; | |
__END__ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl | |
use warnings; | |
use strict; | |
use Getopt::Long; | |
my ($commit, $force, $unfetched, $effectivebranch); | |
GetOptions( | |
'commit!' => \$commit, | |
'force!' => \$force, | |
'allow-unfetched!' => \$unfetched, | |
'behave-as=s' => \$effectivebranch, | |
'help|?' => sub { | |
print <<' HELP'; | |
git release - easy annotated tags for concurrent dev on multiple branches | |
Usage: | |
git release [flags] | |
Flags: | |
-c, --commit, --nocommit (default) | |
If there are modifications to the working copy, then perform a commit | |
prior to tagging the release. If there are no modifications, then this | |
flag has no effect. Note that, if this is provided, the commit MUST be | |
successful or the entire process will fail. This is true regardless of | |
whether --force (see below) is passed or not. | |
If you specify --nocommit, the process will only fail if there are | |
uncommitted changes that need to be made. | |
NOTE: The commit that is run here will add untracked files as well | |
(i.e., it is run with an implicit `git add -A`). If you would like to | |
add only specific files, please manually commit your changes. | |
-f, --force, --noforce (default) | |
Tag even when there is a modification to the working copy of the | |
repository. --noforce refuses to tag if here is a modification. | |
-a, --allow-unfetched, --noallow-unfetched | |
Tag even if some remotes cannot be synchronized. This command tries to | |
fetch remote tags before running to ensure no collisions are made. If | |
you are currently without network and are sure your tagname won't | |
collide, you can use this option. --noallow-unfetched will bail out if | |
you cannot synchronize and is the default. | |
-b BRANCH, --behave-as=BRANCH | |
If you are on a branch that does not have a release prefix configured, | |
then normally the command will fail, and if you are on a branch that | |
does have one configured, you have to use that release prefix. If | |
you'd like to change the default behavior, you can specify a branch | |
name here and it will run the command as though you're currently on | |
that branch. | |
Note that, if the supplied branch name does not have a release prefix | |
configured, the command will still fail. This option also does not | |
affect the auto-merging process that happens at the beginning of the | |
release process; it always operates on the real current branch. | |
-h, -? | |
Print this text and exit successfully. | |
--help | |
Print underlying git config information about how this command is | |
constructed. | |
HELP | |
exit(0); | |
}, | |
); | |
my ($realbranch, $prefix, $relcmd, $relnum, $c, $exit_status); | |
# pull all changes from the remotes (which includes updated tags) - if any of | |
# the remotes fail to synchronize and the user hasn't specified -a or | |
# --allow-unfetched, then stop here. | |
system('git fetch --all 2>&1'); | |
if ($? && !$unfetched) { | |
print STDERR <<' ERROR'; | |
Some remotes could not be synchronized. If you would like to release anyway, | |
please run this command with -a or --allow-unfetched. | |
ERROR | |
exit(1); | |
} | |
# fetch the current branch name to decide our next course of action | |
$realbranch = `git current-branch`; | |
chomp($realbranch); | |
foreach my $remote (split(/\n/, `git remote`)) { | |
system('git', 'merge', '--ff-only', "$remote/$realbranch"); | |
if ($?) { | |
print STDERR <<" ERROR"; | |
You have diverged from the "$realbranch" branch on $remote. | |
Please resolve differences in your history before running this command again. | |
ERROR | |
exit(1); | |
} | |
} | |
# get the current working status of the repository. if the user has modified | |
# the status and hasn't specified --force (or -f), stop right now. | |
`test \$(git status --porcelain | wc -l) -eq 0`; | |
if ($? && $commit) { | |
my (@untracked); | |
my ($fail) = sub { | |
print STDERR <<' ERROR'; | |
You specified that you would like to commit before tagging, but the commit | |
process exited prematurely. Please examine any output errors and correct them | |
before attempting to run this command again. | |
ERROR | |
foreach my $untracked (@untracked) { | |
system('git', 'reset', 'HEAD', $untracked); | |
} | |
exit(1); | |
}; | |
@untracked = split(/\n/, `git ls-untracked`); | |
$fail->() if $?; | |
# add untracked files | |
foreach my $untracked (@untracked) { | |
system('git', 'add', $untracked); | |
$fail->() if $?; | |
} | |
system(qw(git commit -a)); | |
$fail->() if $?; | |
} | |
elsif ($? && !$force) { | |
print STDERR <<' ERROR'; | |
You have uncommitted modifications. If you would like to release anyway, | |
please run this command with -f/--force or -c/--commit. | |
ERROR | |
exit(1); | |
} | |
# if we don't have a branch from --behave-as, then use our real branch, and | |
# escape it so we can stick it into quotation marks for `git config`. | |
$effectivebranch ||= $realbranch; | |
$effectivebranch =~ s/(["\\])/\\$1/g; | |
# now that we've got a safe branch name, try to get the prefix! | |
$prefix = `git config --get release."$effectivebranch".prefix`; | |
chomp($prefix); | |
unless ($prefix) { | |
print <<' ERROR'; | |
This command can only be used on branches with an appropriate configuration | |
value in "release.BRANCHNAME.prefix". | |
Please merge your work to one of these branches, or manually tag your work. | |
You can also use --behave-as to change the effective branch for tagging (see | |
`git release` -h for more details). | |
branch. | |
ERROR | |
exit(1); | |
} | |
# escape slashes (only special char we currently use in tag prefixes) and get | |
# the count of dashes to add onto 2 (the default field position for | |
# "release-###" format numbers) | |
$prefix =~ s/(\\|\/)/\\$1/g; | |
$c = scalar(() = $prefix =~ /\-/) + 2; | |
# build the command to fetch the most recent release tag number and run it, | |
# then increment that value by one to get the newest tag number. | |
$relcmd = "git tag | grep -e '^$prefix-[0-9]\\{1,\\}\$' | cut -d\\- -f$c | " . | |
"sort -rn | head -n1"; | |
$relnum = (`$relcmd` || 0) + 1; | |
# run the tag command and, if it successfully executed (ie, the user typed a | |
# tag message), push the commits and all the tags to the remotes. | |
system("git tag -a $prefix-$relnum"); | |
# push to each remote, with tags | |
foreach my $remote (split(/\n/, `git remote`)) { | |
system('git', 'push', $remote, $realbranch, '--tags'); | |
$exit_status = 1 if $?; | |
} | |
# if any push commands fail, we'll exit with a fail status. otherwise, we're | |
# assuming success and exiting as such. | |
exit($exit_status || 0); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment