[PATCH] repost: add git-nntp-post for posting git commit diffs as news messages

From: Artem Khodush <greenkaa@gmail.com>
Date: 2005-12-14 06:55:09
Sorry for posting again, the previous post has broken long lines in the patch.

In case anyone is still interested, here is a script
that exports git repository to a nntp server,
as prompted in http://marc.theaimsgroup.com/?l=git&m=113385203614980&w=2
Tested with INN on localhost with a copy of git repository
(newsgroups must be created beforehand, and inn.conf artcutoff:
and expire.ctl /remember/: parameters better set to never).

I'm not sure if I got this one right, though:

> A merge commit would probably become a multipart with usually 2
> attachments (but N attachments for a N-way octopus), showing
> diff from each branch.

Currently for merges, it posts diff with parent[0] as first attachment,
and diff between parent[i] and git-merge-base parent[i] parent[0]
as each next attachment.




Signed-off-by: Artem Khodush <greenkaa@gmail.com>

---

 Makefile           |    3
 git-nntp-post.perl |  492 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 494 insertions(+), 1 deletions(-)
 create mode 100755 git-nntp-post.perl

676443e459c0689a8b1ec6c6d46423c4f1bbac8e
diff --git a/Makefile b/Makefile
index 00521fe..2cb2a07 100644
--- a/Makefile
+++ b/Makefile
@@ -97,7 +97,8 @@ SCRIPT_SH = \
 SCRIPT_PERL = \
  git-archimport.perl git-cvsimport.perl git-relink.perl \
  git-shortlog.perl git-fmt-merge-msg.perl \
- git-svnimport.perl git-mv.perl git-cvsexportcommit.perl
+ git-svnimport.perl git-mv.perl git-cvsexportcommit.perl \
+ git-nntp-post.perl

 SCRIPT_PYTHON = \
  git-merge-recursive.py
diff --git a/git-nntp-post.perl b/git-nntp-post.perl
new file mode 100755
index 0000000..5a23faf
--- /dev/null
+++ b/git-nntp-post.perl
@@ -0,0 +1,492 @@
+#!/usr/bin/perl -w
+#
+# Copyright (C) 2005 Artem Khodush <greenkaa@gmail.com>
+#
+# This program contains parts from gitweb.cgi,
+# (C) 2005, Kay Sievers <kay.sievers@vrfy.org>
+# (C) 2005, Christian Gierke <ch@gierke.de>
+
+# This program is licensed under the GPL v2, or a later version
+
+use warnings;
+use strict;
+use Net::NNTP;
+use HTTP::Date;
+use MIME::Lite;
+#use MIME::Entity; # interchangeable with the above, but slower
+
+BEGIN {
+       if( $^V ge v5.8.0 ) {
+               require Encode; import Encode;
+       }else {
+               no strict "refs";
+               *{"Encode::encode"}=sub { my ($a,$s,$b)=@_; return $s; };
+       }
+}
+
+# Read commits, starting from heads given on the command line,
+# (or all repository heads), and post each commit diff
+# to a newsgroup, starting with earliest commit.
+# Commits from different branches go to different newsgroups,
+# named after the head names.
+# The order of heads on the command line is important:
+# when branching point is encountered, the branch specified
+# earlier is considered to be the trunk, continuing into the past.
+
+# For commits with more than one parent, multipart message
+# is posted with first part containing diff with parent[0], and each
+# other part containing diff between parent[i] and
+# git-merge-base parent[i] parent[0] (thus showing summary change
+# followed by what was merged from each branch)
+
+# Subset of the git-rev-list options (--max-age, --min-age and
+# commits starting with ^) is accepted to limit the number of
+# commits posted.
+# Exactly one (the latest) commit may be posted with --one option.
+
+# By default, it posts to localhost nntp port (119), this may
+# be changed by --nntp-host and --nntp-port.
+
+# The newsgroup names are formed by taking the GIT_DIR
+# without the trailing .git, replacing / with ., and appending
+# the head name. This can be overriden with --newsgroup-prefix.
+
+# Each posted message gets an id of the form
+# <commit.SHA1@kernel.org>, and the nntp server is relied upon
+# to reject duplicate posts, and to keep the posting order intact.
+# Hence, with ordinary news servers, each commit
+# can appear only in the newsgroups it was originally posted
+# (currently only one), unless someone care to implement
+# logic for rewriting posts, or a special nntp server.
+
+sub usage() {
+       print STDERR <<EOT;
+$0 [OPTIONS] [<head> ...] [^<commit> ...]
+--nntp-host <host>
+--nntp-port <port>
+--newsgroup-prefix <prefix>
+--max-age <epoch>
+--min-age <epoch>
+--one
+--print-newsgroup-names
+--print-messages
+EOT
+       exit(1);
+}
+
+my $GIT_DIR = `git rev-parse --git-dir`;
+chop $GIT_DIR;
+exit 1 if $?; # rev-parse would have given "not a git dir" message.
+chomp($GIT_DIR);
+
+my $DOMAIN_ID="kernel.org"; # sufficiently unique string for Path
+ #and Message-Id headers
+
+my $NNTP_HOST="localhost";
+my $NNTP_PORT="119";
+my $NEWSGROUP_PREFIX;
+my $MAX_AGE;
+my $MIN_AGE;
+my $ONLY_ONE;
+my %HEADS;
+my @STOP_COMMITS;
+my $PRINT_NEWSGROUP_NAMES;
+my $PRINT_MESSAGES;
+
+my $HEAD_COUNT=0;
+
+while( my $arg=shift ) {
+       if( $arg eq "-h" || $arg eq "--nntp-host" ) {
+               $NNTP_HOST=shift or usage();
+       }elsif( $arg=~/^--nntp-host=(.+)$/ ) {
+               $NNTP_HOST=$1;
+       }elsif( $arg eq "-p" || $arg eq "--nntp-port" ) {
+               $NNTP_PORT=shift or usage();
+       }elsif( $arg=~/^--nntp-port=(.+)$/ ) {
+               $NNTP_PORT=$1;
+       }elsif( $arg eq "-n" || $arg eq "--newsgroup-prefix" ) {
+               $NEWSGROUP_PREFIX=shift or usage();
+       }elsif( $arg=~/^--newsgroup-prefix=(.+)$/ ) {
+               $NEWSGROUP_PREFIX=$1;
+       }elsif( $arg eq "-a" || $arg eq "--max-age" ) {
+               $MAX_AGE=shift or usage();
+       }elsif( $arg=~/^--max-age=(.+)$/ ) {
+               $MAX_AGE=$1;
+       }elsif( $arg eq "-i" || $arg eq "--min-age" ) {
+               $MIN_AGE=shift or usage();
+       }elsif( $arg=~/^--min-age=(.+)$/ ) {
+               $MIN_AGE=$1;
+       }elsif( $arg eq "-1" || $arg eq "--one" ) {
+               $ONLY_ONE=1;
+       }elsif( $arg eq "--print-newsgroup-names" ) {
+               $PRINT_NEWSGROUP_NAMES=1;
+       }elsif( $arg eq "--print-messages" ) {
+               $PRINT_MESSAGES=1;
+       }elsif( $arg=~/^\^/ ) {
+               push @STOP_COMMITS, $arg;
+       }else {
+               if( "HEAD" eq $arg ) {
+                       (undef,$arg)=split( " ", `git-name-rev HEAD` );
+               }
+               $HEADS{$arg}->{order}=++$HEAD_COUNT;
+       }
+}
+
+&verify_heads;
+&read_commits;
+&propagate_branches;
+&post_commits;
+exit( 0 );
+
+sub verify_heads()
+{
+       my $repo=$GIT_DIR;
+       my $read_all_heads= 0==$HEAD_COUNT;
+       open my $fd, "-|", "git-peek-remote $repo"
+        or die "$0: error running git-peek-remote: $!";
+       while( my $line=<$fd> ) {
+               my ($id,$name)=split ' ', $line;
+               my $is_head=0;
+               if( "HEAD" eq $name ) {
+                       $is_head=1;
+                       (undef,$name)=split( " ", `git-name-rev HEAD` );
+               }
+               if( $name=~s/^refs\/heads\/// || $is_head ) {
+ # if there were heads given on the command line,
+ # take only those. Otherwise, with --one, take only
+ # HEAD, without --one, take all heads.
+                       if( exists( $HEADS{$name} )
+   || ($read_all_heads && ($is_head || !$ONLY_ONE)) ) {
+                               $HEADS{$name}->{id}=$id;
+                       }
+               }
+       }
+       close $fd or die "$0: git_get_type: unable to close fd: $!";
+       if( $read_all_heads ) {
+               #sort them
+               my $n=0;
+               for my $name (sort keys %HEADS) {
+                       $HEADS{$name}->{order}=++$n;
+               }
+               # make master the first
+               $HEADS{"master"}->{order}=0 if exists $HEADS{"master"};
+       }
+       if( $ONLY_ONE && scalar( keys %HEADS )!=1 ) {
+               die "$0: --one requires exactly one head, but "
+        . scalar( keys %HEADS ) . " were given\n";
+       }
+       if( $PRINT_NEWSGROUP_NAMES ) { # just print them
+               for my $head (keys %HEADS) {
+                       print &make_newsgroup_name( $head ) . "\n";
+               }
+               exit( 0 );
+       }
+}
+
+my %COMMITS;
+
+sub read_commits
+{
+       my $args="";
+       $args.=" --max-age $MAX_AGE" if $MAX_AGE;
+       $args.=" " . join( " ", keys %HEADS );
+       $args.=" " . join( " ", @STOP_COMMITS );
+       $args.=" --max-count=1" if $ONLY_ONE;
+       $/ = "\0";
+       open my $fd, "-|", "git-rev-list --header --parents $args"
+        or die "$0: error running git-rev-list: $!";
+       while( my $commit_line=<$fd> ) {
+               $commit_line =~ s/\r$//;
+               my @commit_lines = split '\n', $commit_line;
+               pop @commit_lines;
+               my %co;
+
+               my $header = shift @commit_lines;
+               if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
+                       next;
+               }
+               ($co{'id'}, my @parents) = split ' ', $header;
+               $co{'parents'} = \@parents;
+               while (my $line = shift @commit_lines) {
+                       last if $line eq "\n";
+                       if ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
+                               $co{'author'} = $1;
+                               $co{'author_epoch'} = $2;
+                               $co{'author_tz'} = $3;
+                       }elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
+                               $co{'committer'} = $1;
+                               $co{'committer_epoch'} = $2;
+                               $co{'committer_tz'} = $3;
+                       }
+               }
+               $co{'title'}="";
+               $co{'title_short'}="";
+               foreach my $title (@commit_lines) {
+                       if ($title ne "") {
+                               $title=~s/^\s+//;
+                               $co{'title'}=chop_str($title, 80, 5);
+                               $co{'title_short'}=make_title_short( $title );
+                               last;
+                       }
+               }
+               # remove added spaces
+               foreach my $line (@commit_lines) {
+                       $line =~ s/^    //;
+               }
+               $co{'comment'} = \@commit_lines;
+               $COMMITS{$co{'id'}}=\%co;
+       }
+       close $fd or die "$0: git_read_commit: unable to close fd: $!";
+       $/ = "\n";
+}
+
+sub make_title_short
+{
+ my $title=shift;
+       # remove leading stuff of merges
+       # to make the interesting part visible
+       if (length($title) > 50) {
+        $title =~ s/^Automatic //;
+        $title =~ s/^merge (of|with) /Merge ... /i;
+        if (length($title) > 50) {
+        $title =~ s/(http|rsync):\/\///;
+        }
+        if (length($title) > 50) {
+        $title =~ s/(master|www|rsync)\.//;
+        }
+        if (length($title) > 50) {
+        $title =~ s/kernel.org:?//;
+        }
+        if (length($title) > 50) {
+        $title =~ s/\/pub\/scm//;
+        }
+       }
+ return chop_str($title, 50, 5)
+}
+
+sub propagate_branches
+{
+       for my $head (sort { $HEADS{$a}->{order} - $HEADS{$b}->{order} }
+        keys %HEADS) {
+               my $child_id=undef;
+               my $id=$HEADS{$head}->{id};
+               while( $id && exists $COMMITS{$id}
+            && !exists $COMMITS{$id}->{branch} ) {
+                       my $co=$COMMITS{$id};
+                       $co->{branch}=$head;
+                       $co->{child}=$child_id;
+                       $HEADS{$head}->{start_id}=$id;
+                       $child_id=$id;
+                       $id=$co->{parents}->[0];
+               }
+       }
+}
+
+sub make_message_id
+{
+       my $id=shift;
+       return "<commit.$id\@$DOMAIN_ID>",
+}
+
+sub make_commit_body
+{
+       my ($id,$other_id,$from_branch)=@_;
+       my $body="";
+       my ($author, $author_epoch, $committer, $committer_epoch);
+       my $comment="";
+       if( exists $COMMITS{$id} ) {
+        # if we have it already, spare git-cat-file
+               my $co=$COMMITS{$id};
+               $author=$co->{author};
+        $author_epoch=$co->{author_epoch};
+        $committer=$co->{committer};
+        $committer_epoch=$co->{committer_epoch};
+               $comment=join( "\n", @{$co->{comment}} );
+       }else {
+               open my $fd0, "-|", "git-cat-file commit $id"
+        or die "$0: error running git-cat-file: $!";
+               my $header=1;
+               while( <$fd0> ) {
+                       if( !$header ) {
+                               $comment.=$_;
+                       }elsif( m/^\s+$/ ) {
+                               $header=0;
+                       }elsif( m/^author (.*) ([0-9]+) (.*)$/ ) {
+                               $author=$1;
+                               $author_epoch=$2;
+                       }elsif( m/^committer (.*) ([0-9]+) (.*)$/ ) {
+                               $committer=$1;
+                               $committer_epoch=$2;
+                       }
+               }
+               close $fd0;
+       }
+       if( $from_branch ) {
+               my $merge_base=`git-merge-base $id $other_id`;
+               chop $merge_base;
+               $body.="merged from $id\n\n---\n";
+               $other_id=$merge_base if $merge_base;
+       }else {
+               my ($author_name,$author_email)=split( " ", $author );
+               my ($committer_name,$committer_email)=split( " ", $committer );
+               $body.="committer $committer ".time2str($committer_epoch)."\n"
+        unless $author_name eq $committer_name;
+               $body.=$comment;
+               $body.="\n---\n\n";
+       }
+       if( $other_id ) {
+               open my $fd1, "-|",
+ "git-diff-tree -p $other_id $id | git-apply --stat --summary"
+ or die "$0: error running git-diff-tree: $!";
+               while( <$fd1> ) {
+                       $body.=$_;
+               }
+               close $fd1;
+               $body.="\n";
+               open my $fd2, "-|", "git-diff-tree -p $other_id $id"
+        or die "$0: error running git-diff-tree again: $!";
+               while( <$fd2> ) {
+                       $body.=$_;
+               }
+               close $fd2;
+       }
+       return $body;
+}
+
+sub make_commit_message
+{
+       my ($newsgroup,$co)=@_;
+       my $n_parents=scalar( @{$co->{parents}} );
+       my $parents=join(" ", map( make_message_id( $_ ),  @{$co->{parents}}));
+       my $from=$co->{author};
+       $from=$co->{committer} unless $from;
+       $from="unknown author" unless $from;
+       my $subject=$co->{title_short};
+       $subject="undescribed patch" unless $subject;
+       my $msg=MIME::Lite->new(
+               From=>join( " ", map( Encode::encode( "MIME-Q", $_ ),
+        split( " ", $from ) ) ),
+               Subject=>join( " ", map( Encode::encode( "MIME-Q", $_ ),
+        split( " ", $subject ) ) ),
+               Date=>time2str( $co->{author_epoch} ),
+               "Message-Id"=>make_message_id( $co->{id} ),
+               ($n_parents>0 ? (References=>$parents) : ()),
+
+               ($n_parents>1 ? (Type=>"multipart/mixed")
+                       :(
+                       Type=>"text/plain; charset=utf8",
+                       Encoding=>"quoted-printable",
+                       Data=>make_commit_body( $co->{id},
+ $co->{parents}->[0],
+ 0)
+                       )
+               )
+       );
+       $msg->add( Path=>$DOMAIN_ID );
+       $msg->add( Newsgroups=>$newsgroup );
+       if( $n_parents>1 ) {
+               $msg->attach(
+                       Type=>"text/plain; charset=utf8",
+                       Encoding=>"quoted-printable",
+                       Data=>make_commit_body( $co->{id},
+        $co->{parents}->[0],
+        0 )
+               );
+               for my $n (1..$n_parents-1) {
+                       $msg->attach(
+                               Type=>"text/plain; charset=utf8",
+                               Encoding=>"quoted-printable",
+ Data=>make_commit_body( $co->{parents}->[$n],
+        $co->{parents}->[0],
+        1 )
+                       );
+               }
+       }
+       return $msg;
+}
+
+sub post_commit
+{
+       my ($nntp,$newsgroup,$co)=@_;
+       if( $PRINT_MESSAGES ) { # just print them
+               print make_commit_message( $newsgroup, $co )->as_string();
+               print "\0";
+       }else {
+               if( !$nntp->ihave( make_message_id( $co->{id} ) ) ) {
+                       my $reason=$nntp->message();
+                       unless( $reason=~m/got it/i
+     || $reason=~m/duplicate/i ) {
+                               print STDERR "unexpected responce in attempt "
+        . "to post $co->{id}: $reason"
+                       }
+               }else {
+        my$msg=make_commit_message(  $newsgroup, $co );
+                       $nntp->datasend( $msg->as_string() );
+                       if( !$nntp->dataend() ) {
+                               print STDERR "error posting $co->{id}: "
+        . $nntp->message() . "\n";
+                       }
+               }
+       }
+}
+
+sub make_newsgroup_name
+{
+       my $nhead=shift;
+       $nhead=~s/\/+$//;
+       $nhead=~s/^\/+//;
+       $nhead=~s/\./-/g;
+       $nhead=~s/\//\./g;
+       my $nprefix=$NEWSGROUP_PREFIX;
+       if( !defined( $nprefix ) ) {
+               $nprefix=$GIT_DIR;
+               $nprefix=~s/\/\.git\/?\s*$//;
+               $nprefix=~s/\/+$//;
+               $nprefix=~s/^\/+//;
+               $nprefix=~s/\./-/g;
+               $nprefix=~s/\//\./g;
+       }
+       return "$nprefix.$nhead";
+}
+
+sub post_commits
+{
+       my $nntp;
+       unless( $PRINT_MESSAGES ) {
+               $nntp=Net::NNTP->new( $NNTP_HOST,
+        Port=>$NNTP_PORT,
+ Reader=>0 );
+               die "unable to connect to nntp host "
+        ."${NNTP_HOST}:${NNTP_PORT}"
+        unless $nntp;
+       }
+       while( my ($head,$h)=each %HEADS ) {
+               my $id=$h->{start_id};
+               my $newsgroup_name=make_newsgroup_name( $head );
+               while( $id ) {
+                       my $co=$COMMITS{$id};
+                       $id=$co->{child};
+                       next if defined( $MIN_AGE )
+        && $co->{committer_epoch} > $MIN_AGE;
+                       post_commit( $nntp, $newsgroup_name, $co );
+               }
+       }
+}
+
+sub chop_str {
+       my $str = shift;
+       my $len = shift;
+       my $add_len = shift || 10;
+
+       # allow only $len chars, but don't cut a word if it would fit
+       # in $add_len. If it doesn't fit, cut it if it's still longer
+       # than the dots we would add
+       $str =~ m/^(.{0,$len}[^ \/\-_:\.@]{0,$add_len})(.*)/;
+       my $body = $1;
+       my $tail = $2;
+       if (length($tail) > 4) {
+               $tail = " ...";
+       }
+       return "$body$tail";
+}
+
--
0.99.9.GIT
-
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Received on Wed Dec 14 06:55:42 2005

This archive was generated by hypermail 2.1.8 : 2005-12-14 06:56:19 EST