{"thread":{"id":"2813","subject":"[PATCH] add git-nntp-post: post git commit diffs as news messages","startedAt":"2005-12-12T19:39:11Z","lastAt":"2005-12-12T19:39:11Z","messageCount":1,"participants":["Artem Khodush"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"13524","messageId":"40b2b7d90512121139q5508f961y3bdbf5fff2cebc1a@mail.gmail.com","threadId":"2813","inReplyTo":null,"subject":"[PATCH] add git-nntp-post: post git commit diffs as news messages","fromName":"Artem Khodush","fromEmail":"greenkaa@gmail.com","sentAt":"2005-12-12T19:39:11Z","receivedAt":"2005-12-12T19:39:11Z","isPatch":true,"sender":{"key":"greenkaa@gmail.com","avatar":null},"body":"This is a script that exports git repository to a nntp server,\nas prompted in http://marc.theaimsgroup.com/?l=git&m=113385203614980&w=2\nTested with INN on localhost with a copy of git repository\n(newsgroups must be created beforehand, and inn.conf artcutoff:\nand expire.ctl /remember/: parameters better set to never).\n\nI'm not sure if I got this one right, though:\n\n> A merge commit would probably become a multipart with usually 2\n> attachments (but N attachments for a N-way octopus), showing\n> diff from each branch.\n\nCurrently for merges, it posts diff with parent[0] as first attachment,\nand diff between parent[i] and git-merge-base parent[i] parent[0]\nas each next attachment.\n\n\nSigned-off-by: Artem Khodush <greenkaa@gmail.com>\n\n---\n\n Makefile           |    2\n git-nntp-post.perl |  450 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 451 insertions(+), 1 deletions(-)\n create mode 100755 git-nntp-post.perl\n\nbde2562197562d21fb0f66e83e51864c9f648c6a\ndiff --git a/Makefile b/Makefile\nindex a851f56..b9ed12c 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -80,7 +80,7 @@ SCRIPT_SH = \\\n\n SCRIPT_PERL = \\\n \tgit-archimport.perl git-cvsimport.perl git-relink.perl \\\n-\tgit-rename.perl git-shortlog.perl\n+\tgit-rename.perl git-shortlog.perl git-nntp-post.perl\n\n SCRIPT_PYTHON = \\\n \tgit-merge-recursive.py\ndiff --git a/git-nntp-post.perl b/git-nntp-post.perl\nnew file mode 100755\nindex 0000000..13d0293\n--- /dev/null\n+++ b/git-nntp-post.perl\n@@ -0,0 +1,450 @@\n+#!/usr/bin/perl -w\n+#\n+# Copyright (C) 2005 Artem Khodush <greenkaa@gmail.com>\n+#\n+# This program contains parts from gitweb.cgi,\n+# (C) 2005, Kay Sievers <kay.sievers@vrfy.org>\n+# (C) 2005, Christian Gierke <ch@gierke.de>\n+\n+# This program is licensed under the GPL v2, or a later version\n+\n+use warnings;\n+use strict;\n+use Net::NNTP;\n+use HTTP::Date;\n+use MIME::Lite;\n+#use MIME::Entity; # interchangeable with the above, but slower\n+\n+BEGIN {\n+\tif( $^V ge v5.8.0 ) {\n+\t\trequire Encode; import Encode;\n+\t}else {\n+\t\tno strict \"refs\";\n+\t\t*{\"Encode::encode\"}=sub { my ($a,$s,$b)=@_; return $s; };\n+\t}\n+}\n+\n+# Read commits, starting from heads given on the command line,\n+# (or all repository heads), and post each commit diff\n+# to a newsgroup, starting with earliest commit.\n+# Commits from different branches go to different newsgroups,\n+# named after the head names.\n+# The order of heads on the command line is important:\n+# when branching point is encountered, the branch specified\n+# earlier is considered to be the trunk, continuing into the past.\n+\n+# For commits with more than one parent, multipart message\n+# is posted with first part containing diff with parent[0], and each\n+# other part containing diff between parent[i] and\n+# git-merge-base parent[i] parent[0] (thus showing summary change\n+# followed by what was merged from each branch)\n+\n+# Subset of the git-rev-list options (--max-age, --min-age and\n+# commits starting with ^) is accepted to limit the number of\n+# commits posted.\n+# Exactly one (the latest) commit may be posted with --one option.\n+\n+# By default, it posts to localhost nntp port (119), this may\n+# be changed by --nntp-host and --nntp-port.\n+\n+# The newsgroup names are formed by taking the GIT_DIR\n+# without the trailing .git, replacing / with ., and appending\n+# the head name. This can be overriden with --newsgroup-prefix.\n+\n+# Each posted message gets an id of the form\n+# <commit.SHA1@kernel.org>, and the nntp server is relied upon\n+# to reject duplicate posts, and to keep the posting order intact.\n+# Hence, with ordinary news servers, each commit\n+# can appear only in the newsgroups it was originally posted\n+# (currently only one), unless someone care to implement\n+# logic for rewriting posts, or a special nntp server.\n+\n+sub usage() {\n+\tprint STDERR <<EOT;\n+$0 [OPTIONS] [<head> ...] [^<commit> ...]\n+--nntp-host <host>\n+--nntp-port <port>\n+--newsgroup-prefix <prefix>\n+--max-age <epoch>\n+--min-age <epoch>\n+--one\n+--print-newsgroup-names\n+--print-messages\n+EOT\n+\texit(1);\n+}\n+\n+my $GIT_DIR = `git rev-parse --git-dir`;\n+chop $GIT_DIR;\n+exit 1 if $?; # rev-parse would have given \"not a git dir\" message.\n+chomp($GIT_DIR);\n+\n+my $DOMAIN_ID=\"kernel.org\"; # sufficiently unique string for Path and\nMessage-Id headers\n+\n+my $NNTP_HOST=\"localhost\";\n+my $NNTP_PORT=\"119\";\n+my $NEWSGROUP_PREFIX;\n+my $MAX_AGE;\n+my $MIN_AGE;\n+my $ONLY_ONE;\n+my %HEADS;\n+my @STOP_COMMITS;\n+my $PRINT_NEWSGROUP_NAMES;\n+my $PRINT_MESSAGES;\n+\n+my $HEAD_COUNT=0;\n+\n+while( my $arg=shift ) {\n+\tif( $arg eq \"-h\" || $arg eq \"--nntp-host\" ) {\n+\t\t$NNTP_HOST=shift or usage();\n+\t}elsif( $arg=~/^--nntp-host=(.+)$/ ) {\n+\t\t$NNTP_HOST=$1;\n+\t}elsif( $arg eq \"-p\" || $arg eq \"--nntp-port\" ) {\n+\t\t$NNTP_PORT=shift or usage();\n+\t}elsif( $arg=~/^--nntp-port=(.+)$/ ) {\n+\t\t$NNTP_PORT=$1;\n+\t}elsif( $arg eq \"-n\" || $arg eq \"--newsgroup-prefix\" ) {\n+\t\t$NEWSGROUP_PREFIX=shift or usage();\n+\t}elsif( $arg=~/^--newsgroup-prefix=(.+)$/ ) {\n+\t\t$NEWSGROUP_PREFIX=$1;\n+\t}elsif( $arg eq \"-a\" || $arg eq \"--max-age\" ) {\n+\t\t$MAX_AGE=shift or usage();\n+\t}elsif( $arg=~/^--max-age=(.+)$/ ) {\n+\t\t$MAX_AGE=$1;\n+\t}elsif( $arg eq \"-i\" || $arg eq \"--min-age\" ) {\n+\t\t$MIN_AGE=shift or usage();\n+\t}elsif( $arg=~/^--min-age=(.+)$/ ) {\n+\t\t$MIN_AGE=$1;\n+\t}elsif( $arg eq \"-1\" || $arg eq \"--one\" ) {\n+\t\t$ONLY_ONE=1;\n+\t}elsif( $arg eq \"--print-newsgroup-names\" ) {\n+\t\t$PRINT_NEWSGROUP_NAMES=1;\n+\t}elsif( $arg eq \"--print-messages\" ) {\n+\t\t$PRINT_MESSAGES=1;\n+\t}elsif( $arg=~/^\\^/ ) {\n+\t\tpush @STOP_COMMITS, $arg;\n+\t}else {\n+\t\tif( \"HEAD\" eq $arg ) {\n+\t\t\t(undef,$arg)=split( \" \", `git-name-rev HEAD` );\n+\t\t}\n+\t\t$HEADS{$arg}->{order}=++$HEAD_COUNT;\n+\t}\n+}\n+\n+&verify_heads;\n+&read_commits;\n+&propagate_branches;\n+&post_commits;\n+exit( 0 );\n+\n+sub verify_heads()\n+{\n+\tmy $repo=$GIT_DIR;\n+\tmy $read_all_heads= 0==$HEAD_COUNT;\n+\topen my $fd, \"-|\", \"git-peek-remote $repo\" or die \"$0: error running\ngit-peek-remote: $!\";\n+\twhile( my $line=<$fd> ) {\n+\t\tmy ($id,$name)=split ' ', $line;\n+\t\tmy $is_head=0;\n+\t\tif( \"HEAD\" eq $name ) {\n+\t\t\t$is_head=1;\n+\t\t\t(undef,$name)=split( \" \", `git-name-rev HEAD` );\n+\t\t}\n+\t\tif( $name=~s/^refs\\/heads\\/// || $is_head ) {\n+\t\t\t# if there were heads given on the command line, take only those.\n+\t\t\t# otherwise, with --one, take only HEAD, without --one, take all heads.\n+\t\t\tif( exists( $HEADS{$name} ) || ($read_all_heads && ($is_head ||\n!$ONLY_ONE)) ) {\n+\t\t\t\t$HEADS{$name}->{id}=$id;\n+\t\t\t}\n+\t\t}\n+\t}\n+\tclose $fd or die \"$0: git_get_type: unable to close fd: $!\";\n+\tif( $read_all_heads ) {\n+\t\t#sort them\n+\t\tmy $n=0;\n+\t\tfor my $name (sort keys %HEADS) {\n+\t\t\t$HEADS{$name}->{order}=++$n;\n+\t\t}\n+\t\t# make master the first\n+\t\t$HEADS{\"master\"}->{order}=0 if exists $HEADS{\"master\"};\n+\t}\n+\tif( $ONLY_ONE && scalar( keys %HEADS )!=1 ) {\n+\t\tdie \"$0: --one requires exactly one head, but \" . scalar( keys\n%HEADS ) . \" were given\\n\";\n+\t}\n+\tif( $PRINT_NEWSGROUP_NAMES ) { # just print them\n+\t\tfor my $head (keys %HEADS) {\n+\t\t\tprint &make_newsgroup_name( $head ) . \"\\n\";\n+\t\t}\n+\t\texit( 0 );\n+\t}\n+}\n+\n+my %COMMITS;\n+\n+sub read_commits\n+{\n+\tmy $args=\"\";\n+\t$args.=\" --max-age $MAX_AGE\" if $MAX_AGE;\n+\t$args.=\" \" . join( \" \", keys %HEADS );\n+\t$args.=\" \" . join( \" \", @STOP_COMMITS );\n+\t$args.=\" --max-count=1\" if $ONLY_ONE;\n+\t$/ = \"\\0\";\n+\topen my $fd, \"-|\", \"git-rev-list --header --parents $args\" or die\n\"$0: error running git-rev-list: $!\";\n+\twhile( my $commit_line=<$fd> ) {\n+\t\t$commit_line =~ s/\\r$//;\n+\t\tmy @commit_lines = split '\\n', $commit_line;\n+\t\tpop @commit_lines;\n+\t\tmy %co;\n+\n+\t\tmy $header = shift @commit_lines;\n+\t\tif (!($header =~ m/^[0-9a-fA-F]{40}/)) {\n+\t\t\tnext;\n+\t\t}\n+\t\t($co{'id'}, my @parents) = split ' ', $header;\n+\t\t$co{'parents'} = \\@parents;\n+\t\twhile (my $line = shift @commit_lines) {\n+\t\t\tlast if $line eq \"\\n\";\n+\t\t\tif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {\n+\t\t\t\t$co{'author'} = $1;\n+\t\t\t\t$co{'author_epoch'} = $2;\n+\t\t\t\t$co{'author_tz'} = $3;\n+\t\t\t}elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {\n+\t\t\t\t$co{'committer'} = $1;\n+\t\t\t\t$co{'committer_epoch'} = $2;\n+\t\t\t\t$co{'committer_tz'} = $3;\n+\t\t\t}\n+\t\t}\n+\t\t$co{'title'}=\"\";\n+\t\t$co{'title_short'}=\"\";\n+\t\tforeach my $title (@commit_lines) {\n+\t\t\tif ($title ne \"\") {\n+\t\t\t\t$title=~s/^\\s+//;\n+\t\t\t\t$co{'title'} = chop_str($title, 80, 5);\n+\t\t\t\t# remove leading stuff of merges to make the interesting part visible\n+\t\t\t\tif (length($title) > 50) {\n+\t\t\t\t\t$title =~ s/^Automatic //;\n+\t\t\t\t\t$title =~ s/^merge (of|with) /Merge ... /i;\n+\t\t\t\t\tif (length($title) > 50) {\n+\t\t\t\t\t\t$title =~ s/(http|rsync):\\/\\///;\n+\t\t\t\t\t}\n+\t\t\t\t\tif (length($title) > 50) {\n+\t\t\t\t\t\t$title =~ s/(master|www|rsync)\\.//;\n+\t\t\t\t\t}\n+\t\t\t\t\tif (length($title) > 50) {\n+\t\t\t\t\t\t$title =~ s/kernel.org:?//;\n+\t\t\t\t\t}\n+\t\t\t\t\tif (length($title) > 50) {\n+\t\t\t\t\t\t$title =~ s/\\/pub\\/scm//;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t\t$co{'title_short'} = chop_str($title, 50, 5);\n+\t\t\t\tlast;\n+\t\t\t}\n+\t\t}\n+\t\t# remove added spaces\n+\t\tforeach my $line (@commit_lines) {\n+\t\t\t$line =~ s/^    //;\n+\t\t}\n+\t\t$co{'comment'} = \\@commit_lines;\n+\t\t$COMMITS{$co{'id'}}=\\%co;\n+\t}\n+\tclose $fd or die \"$0: git_read_commit: unable to close fd: $!\";\n+\t$/ = \"\\n\";\n+}\n+\n+sub propagate_branches\n+{\n+\tfor my $head (sort { $HEADS{$a}->{order} - $HEADS{$b}->{order} }\nkeys %HEADS) {\n+\t\tmy $child_id=undef;\n+\t\tmy $id=$HEADS{$head}->{id};\n+\t\twhile( $id && exists $COMMITS{$id} && !exists $COMMITS{$id}->{branch} ) {\n+\t\t\tmy $co=$COMMITS{$id};\n+\t\t\t$co->{branch}=$head;\n+\t\t\t$co->{child}=$child_id;\n+\t\t\t$HEADS{$head}->{start_id}=$id;\n+\t\t\t$child_id=$id;\n+\t\t\t$id=$co->{parents}->[0];\n+\t\t}\n+\t}\n+}\n+\n+sub make_message_id\n+{\n+\tmy $id=shift;\n+\treturn \"<commit.$id\\@$DOMAIN_ID>\",\n+}\n+\n+sub make_commit_body\n+{\n+\tmy ($id,$other_id,$from_branch)=@_;\n+\tmy $body=\"\";\n+\tmy ($author, $author_epoch, $committer, $committer_epoch);\n+\tmy $comment=\"\";\n+\tif( exists $COMMITS{$id} ) { # if we have it already, spare git-cat-file\n+\t\tmy $co=$COMMITS{$id};\n+\t\t($author, $author_epoch, $committer,\n$committer_epoch)=($co->{author}, $co->{author_epoch},\n$co->{committer}, $co->{committer_epoch});\n+\t\t$comment=join( \"\\n\", @{$co->{comment}} );\n+\t}else {\n+\t\topen my $fd0, \"-|\", \"git-cat-file commit $id\" or die \"$0: error\nrunning git-cat-file: $!\";\n+\t\tmy $header=1;\n+\t\twhile( <$fd0> ) {\n+\t\t\tif( !$header ) {\n+\t\t\t\t$comment.=$_;\n+\t\t\t}elsif( m/^\\s+$/ ) {\n+\t\t\t\t$header=0;\n+\t\t\t}elsif( m/^author (.*) ([0-9]+) (.*)$/ ) {\n+\t\t\t\t$author=$1;\n+\t\t\t\t$author_epoch=$2;\n+\t\t\t}elsif( m/^committer (.*) ([0-9]+) (.*)$/ ) {\n+\t\t\t\t$committer=$1;\n+\t\t\t\t$committer_epoch=$2;\n+\t\t\t}\n+\t\t}\n+\t\tclose $fd0;\n+\t}\n+\tif( $from_branch ) {\n+\t\tmy $merge_base=`git-merge-base $id $other_id`;\n+\t\tchop $merge_base;\n+\t\t$body.=\"merged from $id\\n\\n---\\n\";\n+\t\t$other_id=$merge_base if $merge_base;\n+\t}else {\n+\t\tmy ($author_name,$author_email)=split( \" \", $author );\n+\t\tmy ($committer_name,$committer_email)=split( \" \", $committer );\n+\t\t$body.=\"committer $committer \" . time2str( $committer_epoch ) .\n\"\\n\" unless $author_name eq $committer_name;\n+\t\t$body.=$comment;\n+\t\t$body.=\"\\n---\\n\\n\";\n+\t}\n+\tif( $other_id ) {\n+\t\topen my $fd1, \"-|\", \"git-diff-tree -p $other_id $id | git-apply\n--stat --summary\" or die \"$0: error running git-diff-tree: $!\";\n+\t\twhile( <$fd1> ) {\n+\t\t\t$body.=$_;\n+\t\t}\n+\t\tclose $fd1;\n+\t\t$body.=\"\\n\";\n+\t\topen my $fd2, \"-|\", \"git-diff-tree -p $other_id $id\" or die \"$0:\nerror running git-diff-tree again: $!\";\n+\t\twhile( <$fd2> ) {\n+\t\t\t$body.=$_;\n+\t\t}\n+\t\tclose $fd2;\n+\t}\n+\treturn $body;\n+}\n+\n+sub make_commit_message\n+{\n+\tmy ($newsgroup,$co)=@_;\n+\tmy $n_parents=scalar( @{$co->{parents}} );\n+\tmy $parents=join( \" \", map( make_message_id( $_ ),  @{$co->{parents}} ) );\n+\tmy $from=$co->{author};\n+\t$from=$co->{committer} unless $from;\n+\t$from=\"unknown author\" unless $from;\n+\tmy $subject=$co->{title_short};\n+\t$subject=\"undescribed patch\" unless $subject;\n+\tmy $msg=MIME::Lite->new(\n+\t\tFrom=>join( \" \", map( Encode::encode( \"MIME-Q\", $_ ), split( \" \",\n$from ) ) ),\n+\t\tSubject=>join( \" \", map( Encode::encode( \"MIME-Q\", $_ ), split( \"\n\", $subject ) ) ),\n+\t\tDate=>time2str( $co->{author_epoch} ),\n+\t\t\"Message-Id\"=>make_message_id( $co->{id} ),\n+\t\t($n_parents>0 ? (References=>$parents) : ()),\n+\n+\t\t($n_parents>1 ? (Type=>\"multipart/mixed\")\n+\t\t\t:(\n+\t\t\tType=>\"text/plain; charset=utf8\",\n+\t\t\tEncoding=>\"quoted-printable\",\n+\t\t\tData=>make_commit_body( $co->{id}, $co->{parents}->[0], 0 )\n+\t\t\t)\n+\t\t)\n+\t);\n+\t$msg->add( Path=>$DOMAIN_ID );\n+\t$msg->add( Newsgroups=>$newsgroup );\n+\tif( $n_parents>1 ) {\n+\t\t$msg->attach(\n+\t\t\tType=>\"text/plain; charset=utf8\",\n+\t\t\tEncoding=>\"quoted-printable\",\n+\t\t\tData=>make_commit_body( $co->{id}, $co->{parents}->[0], 0 )\n+\t\t);\n+\t\tfor my $n (1..$n_parents-1) {\n+\t\t\t$msg->attach(\n+\t\t\t\tType=>\"text/plain; charset=utf8\",\n+\t\t\t\tEncoding=>\"quoted-printable\",\n+\t\t\t\tData=>make_commit_body( $co->{parents}->[$n], $co->{parents}->[0], 1 )\n+\t\t\t);\n+\t\t}\n+\t}\n+\treturn $msg;\n+}\n+\n+sub post_commit\n+{\n+\tmy ($nntp,$newsgroup,$co)=@_;\n+\tif( $PRINT_MESSAGES ) { # just print them\n+\t\tprint make_commit_message( $newsgroup, $co )->as_string();\n+\t\tprint \"\\0\";\n+\t}else {\n+\t\tif( !$nntp->ihave( make_message_id( $co->{id} ) ) ) {\n+\t\t\tmy $reason=$nntp->message();\n+\t\t\tunless( $reason=~m/got it/i || $reason=~m/duplicate/i ) {\n+\t\t\t\tprint STDERR \"unexpected responce in attempt to post $co->{id}: $reason\"\n+\t\t\t}\n+\t\t}else {\n+\t\t\t$nntp->datasend( make_commit_message(  $newsgroup, $co )->as_string() );\n+\t\t\tif( !$nntp->dataend() ) {\n+\t\t\t\tprint STDERR \"error posting $co->{id}: \" . $nntp->message() . \"\\n\";\n+\t\t\t}\n+\t\t}\n+\t}\n+}\n+\n+sub make_newsgroup_name\n+{\n+\tmy $nhead=shift;\n+\t$nhead=~s/\\/+$//;\n+\t$nhead=~s/^\\/+//;\n+\t$nhead=~s/\\./-/g;\n+\t$nhead=~s/\\//\\./g;\n+\tmy $nprefix=$NEWSGROUP_PREFIX;\n+\tif( !defined( $nprefix ) ) {\n+\t\t$nprefix=$GIT_DIR;\n+\t\t$nprefix=~s/\\/\\.git\\/?\\s*$//;\n+\t\t$nprefix=~s/\\/+$//;\n+\t\t$nprefix=~s/^\\/+//;\n+\t\t$nprefix=~s/\\./-/g;\n+\t\t$nprefix=~s/\\//\\./g;\n+\t}\n+\treturn \"$nprefix.$nhead\";\n+}\n+\n+sub post_commits\n+{\n+\tmy $nntp;\n+\tunless( $PRINT_MESSAGES ) {\n+\t\t$nntp=Net::NNTP->new( $NNTP_HOST, Port=>$NNTP_PORT,  Reader=>0 );\n+\t\tdie \"unable to connect to nntp host ${NNTP_HOST}:${NNTP_PORT}\" unless $nntp;\n+\t}\n+\twhile( my ($head,$h)=each %HEADS ) {\n+\t\tmy $id=$h->{start_id};\n+\t\tmy $newsgroup_name=make_newsgroup_name( $head );\n+\t\twhile( $id ) {\n+\t\t\tmy $co=$COMMITS{$id};\n+\t\t\t$id=$co->{child};\n+\t\t\tnext if defined( $MIN_AGE ) && $co->{committer_epoch} > $MIN_AGE;\n+\t\t\tpost_commit( $nntp, $newsgroup_name, $co );\n+\t\t}\n+\t}\n+}\n+\n+sub chop_str {\n+\tmy $str = shift;\n+\tmy $len = shift;\n+\tmy $add_len = shift || 10;\n+\n+\t# allow only $len chars, but don't cut a word if it would fit in $add_len\n+\t# if it doesn't fit, cut it if it's still longer than the dots we would add\n+\t$str =~ m/^(.{0,$len}[^ \\/\\-_:\\.@]{0,$add_len})(.*)/;\n+\tmy $body = $1;\n+\tmy $tail = $2;\n+\tif (length($tail) > 4) {\n+\t\t$tail = \" ...\";\n+\t}\n+\treturn \"$body$tail\";\n+}\n+\n--\n0.99.9.GIT\n"}]}