Commit 7ce6fba0 authored by Ryan Jackson's avatar Ryan Jackson

gitmail: Improved tag, rewind, rebase support; minor cleanup

- Improved support for lightweight tags
  - Supports create/update/delete
  - Show only current ref pointed to (or previously
    pointed to)

- Show previous value of ref when deleted
  - Useful for recovering from accidental deletions

- Distinguish between rewind and rebase
  - Show only new branch head on rewind
  - Show all commits on rebase if only commit message(s)
    changed (otherwise we wouldn't know about the commit
    at all)

- Minor cleanup (remove duplicate code, etc.)
parent 2d1bfecd
...@@ -19,12 +19,10 @@ ...@@ -19,12 +19,10 @@
# #
# TODO: # TODO:
# Users can add notifications for themselves # Users can add notifications for themselves
# Support branch deletion
# Support commits that remove revisions (ie. are not fast-forwards)
# Support tag creation/deletion/etc.
# #
use strict; use strict;
use IPC::Open2;
use POSIX 'setsid'; use POSIX 'setsid';
use Getopt::Long; use Getopt::Long;
sub get_config($$); sub get_config($$);
...@@ -178,7 +176,8 @@ my $EMPTYREV = "0"x40; ...@@ -178,7 +176,8 @@ my $EMPTYREV = "0"x40;
my $CT_CREATE = "create"; my $CT_CREATE = "create";
my $CT_UPDATE = "update"; my $CT_UPDATE = "update";
my $CT_DELETE = "delete"; my $CT_DELETE = "delete";
my $CT_FORCED_UPDATE = "force-update"; my $CT_REWIND = "rewind";
my $CT_REBASE = "rebase";
# #
# Tired of typing this and getting it wrong # Tired of typing this and getting it wrong
...@@ -188,11 +187,11 @@ my $STDERRNULL = " 2> /dev/null"; ...@@ -188,11 +187,11 @@ my $STDERRNULL = " 2> /dev/null";
###################################################################### ######################################################################
# Function Prototypes # Function Prototypes
###################################################################### ######################################################################
sub change_type($$); sub change_type($$$);
sub ref_type($);
sub rev_type($); sub rev_type($);
sub revparse($); sub revparse($);
sub changed_files($$); sub changed_files(@);
sub changed_files_single_revision($);
sub get_mail_addresses($@); sub get_mail_addresses($@);
sub get_merge_base($$); sub get_merge_base($$);
sub uniq(@); sub uniq(@);
...@@ -202,7 +201,6 @@ sub get_commits($$$); ...@@ -202,7 +201,6 @@ sub get_commits($$$);
sub send_mail($$@); sub send_mail($$@);
sub short_refname($); sub short_refname($);
sub debug(@); sub debug(@);
sub rev_string($$);
sub object_exists($$); sub object_exists($$);
sub filter_out_objects_in_repo($@); sub filter_out_objects_in_repo($@);
...@@ -252,10 +250,17 @@ if ($mailconfigfile) { ...@@ -252,10 +250,17 @@ if ($mailconfigfile) {
# #
my @reflines; my @reflines;
if ($testmode) { if ($testmode) {
my $fullref = `git rev-parse --symbolic-full-name $testbranch`;
if (!$fullref) {
exit(1);
}
my $newrev = `git rev-parse $fullref $STDERRNULL`;
chomp $newrev;
# #
# Provide a simple way to grab some commits - the three most recent ones # Provide a simple way to grab some commits - the three most recent ones
# #
@reflines = ("$testbranch~2 $testbranch $testbranch"); @reflines = ("$newrev~2 $newrev $fullref");
} else { } else {
# #
# Get all of the references that are being pushed from stdin - we do this in # Get all of the references that are being pushed from stdin - we do this in
...@@ -284,6 +289,9 @@ if ($detach && !$debug) { ...@@ -284,6 +289,9 @@ if ($detach && !$debug) {
# Loop over all of the references we got on stdin # Loop over all of the references we got on stdin
# #
foreach my $refline (@reflines) { foreach my $refline (@reflines) {
my @commits;
my @changed_files;
chomp $refline; chomp $refline;
debug("Read line '$refline'"); debug("Read line '$refline'");
...@@ -293,6 +301,7 @@ foreach my $refline (@reflines) { ...@@ -293,6 +301,7 @@ foreach my $refline (@reflines) {
# happened in the middle # happened in the middle
# #
my ($oldrev, $newrev, $refname) = split(/\s+/, $refline); my ($oldrev, $newrev, $refname) = split(/\s+/, $refline);
my $ref_type = ref_type($refname);
# #
# Use rev-parse so that fancy symbolic names, etc. can be used # Use rev-parse so that fancy symbolic names, etc. can be used
...@@ -305,38 +314,57 @@ foreach my $refline (@reflines) { ...@@ -305,38 +314,57 @@ foreach my $refline (@reflines) {
# Figure out what type of change occured (update, creation, deletion, etc.) # Figure out what type of change occured (update, creation, deletion, etc.)
# and what type of objects (commit, tree, etc.) we got # and what type of objects (commit, tree, etc.) we got
# #
my $ct = change_type($oldrev,$newrev); my $ct = change_type($oldrev,$newrev,$ref_type);
my $old_type = rev_type($oldrev); my $old_type = rev_type($oldrev);
my $new_type = rev_type($newrev); my $new_type = rev_type($newrev);
debug("Change type: $ct ($old_type,$new_type)"); debug("Change type: $ct ($old_type,$new_type)");
# #
# For now, only handle commits that update existing branches or make # For now, only handle commit objects. Tag objects require extra work.
# new ones
# #
if ($new_type ne "commit") { if ($new_type && $new_type ne "commit") {
debug("Skipping non-commit"); debug("Skipping non-commit");
next; next;
} }
# #
# Get all commits between these two revisions # Figure out which commits we're interested in based on reference type
# and all files that changed # and change type.
# #
my @commits = get_commits($oldrev,$newrev,$refname); if ($ref_type eq 'tag') {
if ($ct eq $CT_DELETE) {
# We want to know where the tag used to point before deletion
push @commits, $oldrev;
} else {
# Tags only have delete, create, and update. Rewind and rebase
# don't make sense in tag context.
#
# We only care about the new value of the tag here.
push @commits, $newrev;
}
} elsif ($ref_type eq 'branch') {
if ($ct eq $CT_DELETE) {
# We want to know where the branch used to point before deletion
push @commits, $oldrev;
} elsif ($ct eq $CT_REWIND) {
# There's no new history to show, but we still want to see where
# the branch now points.
push @commits, $newrev;
} else {
@commits = get_commits($oldrev,$newrev,$refname);
# We only want to see *new* commits, which means that commits already
# in the main repository need to be excluded too.
if (defined $exclude_repo) {
@commits = filter_out_objects_in_repo($exclude_repo, @commits);
}
}
}
next unless (@commits);
debug("commits are: ", join(" ",@commits)); debug("commits are: ", join(" ",@commits));
my @changed_files; @changed_files = changed_files(@commits);
if (defined $exclude_repo || $ct eq $CT_FORCED_UPDATE) {
if (defined $exclude_repo) {
@commits = filter_out_objects_in_repo($exclude_repo, @commits);
next unless (@commits);
}
@changed_files = map { changed_files_single_revision($_) } @commits;
} else {
@changed_files = changed_files($oldrev,$newrev);
}
debug("Changed files: ", join(",",@changed_files)); debug("Changed files: ", join(",",@changed_files));
# #
...@@ -364,8 +392,8 @@ debug("finishing"); ...@@ -364,8 +392,8 @@ debug("finishing");
# Does this change represent the creation, deletion, or update of an object? # Does this change represent the creation, deletion, or update of an object?
# Takes old and new revs # Takes old and new revs
# #
sub change_type($$) { sub change_type($$$) {
my ($oldrev, $newrev) = @_; my ($oldrev, $newrev, $ref_type) = @_;
# #
# We can detect creates and deletes by looking for a special 'null' # We can detect creates and deletes by looking for a special 'null'
...@@ -375,13 +403,18 @@ sub change_type($$) { ...@@ -375,13 +403,18 @@ sub change_type($$) {
return $CT_CREATE; return $CT_CREATE;
} elsif ($newrev eq $EMPTYREV) { } elsif ($newrev eq $EMPTYREV) {
return $CT_DELETE; return $CT_DELETE;
} elsif ($ref_type eq 'tag') {
return $CT_UPDATE;
} else { } else {
my $merge_base = get_merge_base($oldrev,$newrev); my $merge_base = get_merge_base($oldrev,$newrev);
my $oldrev = revparse($oldrev); my $oldrev = revparse($oldrev);
my $newrev = revparse($newrev);
if ($merge_base eq $oldrev) { if ($merge_base eq $oldrev) {
return $CT_UPDATE; return $CT_UPDATE;
} elsif ($merge_base eq $newrev) {
return $CT_REWIND;
} else { } else {
return $CT_FORCED_UPDATE; return $CT_REBASE;
} }
} }
} }
...@@ -396,6 +429,20 @@ sub rev_type($) { ...@@ -396,6 +429,20 @@ sub rev_type($) {
return $rev_type; return $rev_type;
} }
#
# Find out what type of reference this is
#
sub ref_type($) {
my ($ref) = @_;
my $type;
if ($ref =~ m#^refs/heads/#) {
$type = 'branch';
} elsif ($ref =~ m#^refs/tags/#) {
$type = 'tag';
}
return $type;
}
# #
# Parse (possibly) symbolic revision name into hash # Parse (possibly) symbolic revision name into hash
# Note: Dies if the revision name is bogus! # Note: Dies if the revision name is bogus!
...@@ -413,30 +460,31 @@ sub revparse($) { ...@@ -413,30 +460,31 @@ sub revparse($) {
} }
# #
# Given two revisions, return a list of the files that were changed between # Given a list of commit object hashes, return the list of files changed by
# them # all commits.
# #
sub changed_files($$) { sub changed_files(@) {
my ($oldrev,$newrev) = @_; my %files;
my $revstring = rev_string($oldrev,$newrev); debug("running '$GIT diff-tree --stdin -r --name-only --no-commit-id' on '@_'");
debug("running '$GIT diff-tree -r --name-only '$revstring''"); my $pid = open2(\*OUT, \*IN, "$GIT diff-tree --stdin -r --name-only --no-commit-id");
my @lines = `$GIT diff-tree -r --name-only '$revstring' $STDERRNULL`;
chomp @lines;
return @lines;
}
# print IN "$_\n" for (@_);
# Given one revision, return a list of the files that were changed between close(IN);
# it and its parents
# while (<OUT>) {
sub changed_files_single_revision($) { chomp;
my ($rev) = @_; $files{$_} = 1;
}
close(OUT);
debug("running '$GIT diff-tree -r --name-only '$rev''"); waitpid($pid, 0);
my @lines = `$GIT diff-tree -r --name-only '$rev' $STDERRNULL`; my $rc = $? >> 8;
chomp @lines; if ($rc) {
return @lines; die "'git diff-tree' exited with return code $rc\n";
}
return keys(%files);
} }
# #
...@@ -581,7 +629,7 @@ sub commit_mail($\@$@) { ...@@ -581,7 +629,7 @@ sub commit_mail($\@$@) {
# #
# Construct the subject line. For now, we just say what repo (if defined) # Construct the subject line. For now, we just say what repo (if defined)
# and what branch it happened on # and what branch/tag it happened on
# #
my $subject = "git commit: "; my $subject = "git commit: ";
my $ref_type; my $ref_type;
...@@ -589,31 +637,37 @@ sub commit_mail($\@$@) { ...@@ -589,31 +637,37 @@ sub commit_mail($\@$@) {
$subject .= "[$reponame] "; $subject .= "[$reponame] ";
} }
if ($refname =~ m#refs/tags/#) { $ref_type = ref_type($refname);
$ref_type = 'tag';
} elsif ($refname =~ m#refs/heads/#) {
$ref_type = 'branch';
}
$subject .= $ref_type . ' ' . short_refname($refname); $subject .= $ref_type . ' ' . short_refname($refname);
my $what_happened;
if ($ct eq $CT_UPDATE) { if ($ct eq $CT_UPDATE) {
$subject .= " updated"; $what_happened .= 'updated';
} elsif ($ct eq $CT_FORCED_UPDATE) { } elsif ($ct eq $CT_REWIND) {
$subject .= " force-updated"; $what_happened .= 'rewound';
} elsif ($ct eq $CT_REBASE) {
$what_happened .= 'rebased';
} elsif ($ct eq $CT_CREATE) { } elsif ($ct eq $CT_CREATE) {
$subject .= " created"; $what_happened .= 'created';
} elsif ($ct eq $CT_DELETE) { } elsif ($ct eq $CT_DELETE) {
$subject .= " deleted"; $what_happened .= 'deleted';
} }
$subject .= ' ' . $what_happened;
my $actionstring = ucfirst($ref_type) . ' ' . short_refname($refname) . my $actionstring = ucfirst($ref_type) . ' ' . short_refname($refname) .
" has been ${ct}d\n\n"; " has been $what_happened";
if ($ct eq $CT_FORCED_UPDATE) { if ($ct eq $CT_REBASE) {
$actionstring .= "New and/or modified commits shown below\n\n"; $actionstring .= ". The following commits are new or have been modified:";
} elsif ($ct eq $CT_REWIND) {
$actionstring .= " to point to the following commit:";
} elsif ($ct eq $CT_DELETE) {
$actionstring .= ". It previously pointed to the following commit:";
} }
$actionstring .= "\n\n";
my @fullbody; my @fullbody;
foreach my $rev (@$commits) { foreach my $rev (@$commits) {
# #
...@@ -654,38 +708,47 @@ sub commit_mail($\@$@) { ...@@ -654,38 +708,47 @@ sub commit_mail($\@$@) {
# #
sub get_commits($$$) { sub get_commits($$$) {
my ($oldrev,$newrev,$refname) = @_; my ($oldrev,$newrev,$refname) = @_;
my $ct = change_type($oldrev,$newrev); my $ct = change_type($oldrev,$newrev, ref_type($refname));
# #
# If this is an update, we can just ask git for the revisions between the # If this is an update, we can just ask git for the revisions between the
# two revisions we were given. We call git-cherry for this information # two revisions we were given.
# so that we can identify if a particular commit already exists in the
# repository with a different commit hash (for the rebase case).
# #
if ($ct eq $CT_UPDATE || $ct eq $CT_FORCED_UPDATE) { if ($ct eq $CT_UPDATE) {
my $revstring = "$oldrev..$newrev";
debug("running '$GIT rev-list --reverse --date-order '$revstring'");
my @revs = `$GIT rev-list --reverse --date-order '$revstring'`;
chomp @revs;
return @revs;
} elsif ($ct eq $CT_REBASE) {
debug("running '$GIT cherry '$oldrev' '$newrev'"); debug("running '$GIT cherry '$oldrev' '$newrev'");
# Only return revs prefixed with a '+' since commits prefixed with a # Only return revs prefixed with a '+' since commits prefixed with a
# '-' are already in the repository with a different commit hash. We # '-' are already in the repository with a different commit hash.
# should have seen those already, so we don't want to see them here. #
# Note that '+' commits may not be "new" per-se, but they are not the # The '-' commits are the same as their non-rebased counterparts, except
# same as the existing commit with the same commit message due to # their ancestry is different. For the email messages, we don't care
# rebasing. # about these since we should have seen the original commits already.
#
# The '+' commits are either new or are rebased commits whose *content*
# has changed. We definitely want to see these. Note that this only
# applies to the content of the commit, not the commit message.
my @revs; my @revs;
my @all_revs;
for (`$GIT cherry '$oldrev' '$newrev'`) { for (`$GIT cherry '$oldrev' '$newrev'`) {
debug($_);
chomp; chomp;
unshift @revs, $1 if (/^\+\s+(.*)$/); @_ = split /\s+/, $_;
unshift @revs, $_[1] if ($_[0] eq '+');
unshift @all_revs, $_[1];
} }
return @revs;
# If cherry finds that all of the commits are already present,
# report 'em all anyway. We still need to know that the rebase
# happened, and reporting just the head doesn't make any sense.
@revs = @all_revs if (!@revs);
return @revs;
} elsif ($ct eq $CT_CREATE) { } elsif ($ct eq $CT_CREATE) {
#
# For tags, just return the new revision. This at least tells us
# where the tag points.
#
if ($refname =~ m#^refs/tags/#) {
return ($newrev);
}
# #
# If it's a create, we have to be a bit more fancy: we look for all # If it's a create, we have to be a bit more fancy: we look for all
# commits reachable from the new branch, but *not* reachable from any # commits reachable from the new branch, but *not* reachable from any
...@@ -710,6 +773,12 @@ sub get_commits($$$) { ...@@ -710,6 +773,12 @@ sub get_commits($$$) {
debug("running '$GIT rev-parse --not $other_branches | $GIT rev-list --pretty --stdin $newrev'"); debug("running '$GIT rev-parse --not $other_branches | $GIT rev-list --pretty --stdin $newrev'");
my @commits = `$GIT rev-parse --not $other_branches | $GIT rev-list --reverse --date-order --stdin $newrev`; my @commits = `$GIT rev-parse --not $other_branches | $GIT rev-list --reverse --date-order --stdin $newrev`;
# We always want to be notified when a branch is created, so if there are no commits reachable
# from only this branch just report on the head of the branch.
push @commits, $newrev if (!@commits);
debug("commits are @commits");
chomp @commits; chomp @commits;
return @commits; return @commits;
} }
...@@ -779,9 +848,17 @@ sub short_refname($) { ...@@ -779,9 +848,17 @@ sub short_refname($) {
my $refname = `git rev-parse --abbrev-ref $ref $STDERRNULL`; my $refname = `git rev-parse --abbrev-ref $ref $STDERRNULL`;
chomp $refname; chomp $refname;
# This shouldn't be necessary, but return the full ref if # Fall back to full name if rev-parse fails for some reason
# rev-parse doesn't return anything.
$refname = $ref if (!$refname); $refname = $ref if (!$refname);
debug("got short refname \"$refname\"");
# If the ref didn't get shortened, it may be because it was deleted. Just
# chop off 'refs/heads' or 'refs/tags' and return the rest.
if ($refname =~ m#^refs/(?:heads|tags)/(.*)#) {
$refname = $1;
}
return $refname; return $refname;
} }
...@@ -825,25 +902,6 @@ sub get_config($$) { ...@@ -825,25 +902,6 @@ sub get_config($$) {
} }
} }
#
# Return an appropriate string to get a revision: if the ref was created or
# deleted, this looks a little different
#
sub rev_string($$) {
my ($oldrev, $newrev) = @_;
my $ct = change_type($oldrev,$newrev);
if ($ct eq $CT_UPDATE) {
return "$oldrev..$newrev";
} elsif ($ct eq $CT_CREATE) {
return $newrev;
} elsif ($ct eq $CT_DELETE) {
return $oldrev;
} else {
# Shouldn't be able to get here
return undef;
}
}
# #
# Returns the merge base (i.e., common ancestor) of # Returns the merge base (i.e., common ancestor) of
# the two supplied revisions. # the two supplied revisions.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment