Commit Diff


commit - /dev/null
commit + f60772b95eed41b965f4de47c088eeb81e7bc607
blob - /dev/null
blob + cb089cd89a7d7686d284d8761201649346b5aa1c (mode 644)
--- /dev/null
+++ .git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
blob - /dev/null
blob + 032a7e570146d61b059a5c83b84aa1d76b3dcb7a (mode 644)
--- /dev/null
+++ .git/config
@@ -0,0 +1,11 @@
+[core]
+	repositoryformatversion = 0
+	filemode = true
+	bare = false
+	logallrefupdates = true
+[remote "origin"]
+	url = git://ircforever.org/ticl
+	fetch = +refs/heads/*:refs/remotes/origin/*
+[branch "master"]
+	remote = origin
+	merge = refs/heads/master
blob - /dev/null
blob + 498b267a8c7812490d6479839c5577eaaec79d62 (mode 644)
--- /dev/null
+++ .git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
blob - /dev/null
blob + a5d7b84a673458d14d9aab082183a1968c2c7492 (mode 755)
--- /dev/null
+++ .git/hooks/applypatch-msg.sample
@@ -0,0 +1,15 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message taken by
+# applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.  The hook is
+# allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "applypatch-msg".
+
+. git-sh-setup
+commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
+test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
+:
blob - /dev/null
blob + b58d1184a9d43a39c0d95f32453efc78581877d6 (mode 755)
--- /dev/null
+++ .git/hooks/commit-msg.sample
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message.
+# Called by "git commit" with one argument, the name of the file
+# that has the commit message.  The hook should exit with non-zero
+# status after issuing an appropriate message if it wants to stop the
+# commit.  The hook is allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "commit-msg".
+
+# Uncomment the below to add a Signed-off-by line to the message.
+# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
+# hook is more suited to it.
+#
+# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
+
+# This example catches duplicate Signed-off-by lines.
+
+test "" = "$(grep '^Signed-off-by: ' "$1" |
+	 sort | uniq -c | sed -e '/^[ 	]*1[ 	]/d')" || {
+	echo >&2 Duplicate Signed-off-by lines.
+	exit 1
+}
blob - /dev/null
blob + 23e856f5deeb7f564afc22f2beed54449c2d3afb (mode 755)
--- /dev/null
+++ .git/hooks/fsmonitor-watchman.sample
@@ -0,0 +1,174 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use IPC::Open2;
+
+# An example hook script to integrate Watchman
+# (https://facebook.github.io/watchman/) with git to speed up detecting
+# new and modified files.
+#
+# The hook is passed a version (currently 2) and last update token
+# formatted as a string and outputs to stdout a new update token and
+# all files that have been modified since the update token. Paths must
+# be relative to the root of the working tree and separated by a single NUL.
+#
+# To enable this hook, rename this file to "query-watchman" and set
+# 'git config core.fsmonitor .git/hooks/query-watchman'
+#
+my ($version, $last_update_token) = @ARGV;
+
+# Uncomment for debugging
+# print STDERR "$0 $version $last_update_token\n";
+
+# Check the hook interface version
+if ($version ne 2) {
+	die "Unsupported query-fsmonitor hook version '$version'.\n" .
+	    "Falling back to scanning...\n";
+}
+
+my $git_work_tree = get_working_dir();
+
+my $retry = 1;
+
+my $json_pkg;
+eval {
+	require JSON::XS;
+	$json_pkg = "JSON::XS";
+	1;
+} or do {
+	require JSON::PP;
+	$json_pkg = "JSON::PP";
+};
+
+launch_watchman();
+
+sub launch_watchman {
+	my $o = watchman_query();
+	if (is_work_tree_watched($o)) {
+		output_result($o->{clock}, @{$o->{files}});
+	}
+}
+
+sub output_result {
+	my ($clockid, @files) = @_;
+
+	# Uncomment for debugging watchman output
+	# open (my $fh, ">", ".git/watchman-output.out");
+	# binmode $fh, ":utf8";
+	# print $fh "$clockid\n@files\n";
+	# close $fh;
+
+	binmode STDOUT, ":utf8";
+	print $clockid;
+	print "\0";
+	local $, = "\0";
+	print @files;
+}
+
+sub watchman_clock {
+	my $response = qx/watchman clock "$git_work_tree"/;
+	die "Failed to get clock id on '$git_work_tree'.\n" .
+		"Falling back to scanning...\n" if $? != 0;
+
+	return $json_pkg->new->utf8->decode($response);
+}
+
+sub watchman_query {
+	my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
+	or die "open2() failed: $!\n" .
+	"Falling back to scanning...\n";
+
+	# In the query expression below we're asking for names of files that
+	# changed since $last_update_token but not from the .git folder.
+	#
+	# To accomplish this, we're using the "since" generator to use the
+	# recency index to select candidate nodes and "fields" to limit the
+	# output to file names only. Then we're using the "expression" term to
+	# further constrain the results.
+	my $last_update_line = "";
+	if (substr($last_update_token, 0, 1) eq "c") {
+		$last_update_token = "\"$last_update_token\"";
+		$last_update_line = qq[\n"since": $last_update_token,];
+	}
+	my $query = <<"	END";
+		["query", "$git_work_tree", {$last_update_line
+			"fields": ["name"],
+			"expression": ["not", ["dirname", ".git"]]
+		}]
+	END
+
+	# Uncomment for debugging the watchman query
+	# open (my $fh, ">", ".git/watchman-query.json");
+	# print $fh $query;
+	# close $fh;
+
+	print CHLD_IN $query;
+	close CHLD_IN;
+	my $response = do {local $/; <CHLD_OUT>};
+
+	# Uncomment for debugging the watch response
+	# open ($fh, ">", ".git/watchman-response.json");
+	# print $fh $response;
+	# close $fh;
+
+	die "Watchman: command returned no output.\n" .
+	"Falling back to scanning...\n" if $response eq "";
+	die "Watchman: command returned invalid output: $response\n" .
+	"Falling back to scanning...\n" unless $response =~ /^\{/;
+
+	return $json_pkg->new->utf8->decode($response);
+}
+
+sub is_work_tree_watched {
+	my ($output) = @_;
+	my $error = $output->{error};
+	if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
+		$retry--;
+		my $response = qx/watchman watch "$git_work_tree"/;
+		die "Failed to make watchman watch '$git_work_tree'.\n" .
+		    "Falling back to scanning...\n" if $? != 0;
+		$output = $json_pkg->new->utf8->decode($response);
+		$error = $output->{error};
+		die "Watchman: $error.\n" .
+		"Falling back to scanning...\n" if $error;
+
+		# Uncomment for debugging watchman output
+		# open (my $fh, ">", ".git/watchman-output.out");
+		# close $fh;
+
+		# Watchman will always return all files on the first query so
+		# return the fast "everything is dirty" flag to git and do the
+		# Watchman query just to get it over with now so we won't pay
+		# the cost in git to look up each individual file.
+		my $o = watchman_clock();
+		$error = $output->{error};
+
+		die "Watchman: $error.\n" .
+		"Falling back to scanning...\n" if $error;
+
+		output_result($o->{clock}, ("/"));
+		$last_update_token = $o->{clock};
+
+		eval { launch_watchman() };
+		return 0;
+	}
+
+	die "Watchman: $error.\n" .
+	"Falling back to scanning...\n" if $error;
+
+	return 1;
+}
+
+sub get_working_dir {
+	my $working_dir;
+	if ($^O =~ 'msys' || $^O =~ 'cygwin') {
+		$working_dir = Win32::GetCwd();
+		$working_dir =~ tr/\\/\//;
+	} else {
+		require Cwd;
+		$working_dir = Cwd::cwd();
+	}
+
+	return $working_dir;
+}
blob - /dev/null
blob + ff565eb3d8811d04356c590e8d0a5c92288eb30b (mode 644)
--- /dev/null
+++ .git/hooks/post-receive-email.sample
@@ -0,0 +1,759 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Andy Parkins
+#
+# An example hook script to mail out commit update information.
+#
+# NOTE: This script is no longer under active development.  There
+# is another script, git-multimail, which is more capable and
+# configurable and is largely backwards-compatible with this script;
+# please see "contrib/hooks/multimail/".  For instructions on how to
+# migrate from post-receive-email to git-multimail, please see
+# "README.migrate-from-post-receive-email" in that directory.
+#
+# This hook sends emails listing new revisions to the repository
+# introduced by the change being reported.  The rule is that (for
+# branch updates) each commit will appear on one email and one email
+# only.
+#
+# This hook is stored in the contrib/hooks directory.  Your distribution
+# will have put this somewhere standard.  You should make this script
+# executable then link to it in the repository you would like to use it in.
+# For example, on debian the hook is stored in
+# /usr/share/git-core/contrib/hooks/post-receive-email:
+#
+#  cd /path/to/your/repository.git
+#  ln -sf /usr/share/git-core/contrib/hooks/post-receive-email hooks/post-receive
+#
+# This hook script assumes it is enabled on the central repository of a
+# project, with all users pushing only to it and not between each other.  It
+# will still work if you don't operate in that style, but it would become
+# possible for the email to be from someone other than the person doing the
+# push.
+#
+# To help with debugging and use on pre-v1.5.1 git servers, this script will
+# also obey the interface of hooks/update, taking its arguments on the
+# command line.  Unfortunately, hooks/update is called once for each ref.
+# To avoid firing one email per ref, this script just prints its output to
+# the screen when used in this mode.  The output can then be redirected if
+# wanted.
+#
+# Config
+# ------
+# hooks.mailinglist
+#   This is the list that all pushes will go to; leave it blank to not send
+#   emails for every ref update.
+# hooks.announcelist
+#   This is the list that all pushes of annotated tags will go to.  Leave it
+#   blank to default to the mailinglist field.  The announce emails lists
+#   the short log summary of the changes since the last annotated tag.
+# hooks.envelopesender
+#   If set then the -f option is passed to sendmail to allow the envelope
+#   sender address to be set
+# hooks.emailprefix
+#   All emails have their subjects prefixed with this prefix, or "[SCM]"
+#   if emailprefix is unset, to aid filtering
+# hooks.showrev
+#   The shell command used to format each revision in the email, with
+#   "%s" replaced with the commit id.  Defaults to "git rev-list -1
+#   --pretty %s", displaying the commit id, author, date and log
+#   message.  To list full patches separated by a blank line, you
+#   could set this to "git show -C %s; echo".
+#   To list a gitweb/cgit URL *and* a full patch for each change set, use this:
+#     "t=%s; printf 'http://.../?id=%%s' \$t; echo;echo; git show -C \$t; echo"
+#   Be careful if "..." contains things that will be expanded by shell "eval"
+#   or printf.
+# hooks.emailmaxlines
+#   The maximum number of lines that should be included in the generated
+#   email body. If not specified, there is no limit.
+#   Lines beyond the limit are suppressed and counted, and a final
+#   line is added indicating the number of suppressed lines.
+# hooks.diffopts
+#   Alternate options for the git diff-tree invocation that shows changes.
+#   Default is "--stat --summary --find-copies-harder". Add -p to those
+#   options to include a unified diff of changes in addition to the usual
+#   summary output.
+#
+# Notes
+# -----
+# All emails include the headers "X-Git-Refname", "X-Git-Oldrev",
+# "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and
+# give information for debugging.
+#
+
+# ---------------------------- Functions
+
+#
+# Function to prepare for email generation. This decides what type
+# of update this is and whether an email should even be generated.
+#
+prep_for_email()
+{
+	# --- Arguments
+	oldrev=$(git rev-parse $1)
+	newrev=$(git rev-parse $2)
+	refname="$3"
+
+	# --- Interpret
+	# 0000->1234 (create)
+	# 1234->2345 (update)
+	# 2345->0000 (delete)
+	if expr "$oldrev" : '0*$' >/dev/null
+	then
+		change_type="create"
+	else
+		if expr "$newrev" : '0*$' >/dev/null
+		then
+			change_type="delete"
+		else
+			change_type="update"
+		fi
+	fi
+
+	# --- Get the revision types
+	newrev_type=$(git cat-file -t $newrev 2> /dev/null)
+	oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null)
+	case "$change_type" in
+	create|update)
+		rev="$newrev"
+		rev_type="$newrev_type"
+		;;
+	delete)
+		rev="$oldrev"
+		rev_type="$oldrev_type"
+		;;
+	esac
+
+	# The revision type tells us what type the commit is, combined with
+	# the location of the ref we can decide between
+	#  - working branch
+	#  - tracking branch
+	#  - unannoted tag
+	#  - annotated tag
+	case "$refname","$rev_type" in
+		refs/tags/*,commit)
+			# un-annotated tag
+			refname_type="tag"
+			short_refname=${refname##refs/tags/}
+			;;
+		refs/tags/*,tag)
+			# annotated tag
+			refname_type="annotated tag"
+			short_refname=${refname##refs/tags/}
+			# change recipients
+			if [ -n "$announcerecipients" ]; then
+				recipients="$announcerecipients"
+			fi
+			;;
+		refs/heads/*,commit)
+			# branch
+			refname_type="branch"
+			short_refname=${refname##refs/heads/}
+			;;
+		refs/remotes/*,commit)
+			# tracking branch
+			refname_type="tracking branch"
+			short_refname=${refname##refs/remotes/}
+			echo >&2 "*** Push-update of tracking branch, $refname"
+			echo >&2 "***  - no email generated."
+			return 1
+			;;
+		*)
+			# Anything else (is there anything else?)
+			echo >&2 "*** Unknown type of update to $refname ($rev_type)"
+			echo >&2 "***  - no email generated"
+			return 1
+			;;
+	esac
+
+	# Check if we've got anyone to send to
+	if [ -z "$recipients" ]; then
+		case "$refname_type" in
+			"annotated tag")
+				config_name="hooks.announcelist"
+				;;
+			*)
+				config_name="hooks.mailinglist"
+				;;
+		esac
+		echo >&2 "*** $config_name is not set so no email will be sent"
+		echo >&2 "*** for $refname update $oldrev->$newrev"
+		return 1
+	fi
+
+	return 0
+}
+
+#
+# Top level email generation function.  This calls the appropriate
+# body-generation routine after outputting the common header.
+#
+# Note this function doesn't actually generate any email output, that is
+# taken care of by the functions it calls:
+#  - generate_email_header
+#  - generate_create_XXXX_email
+#  - generate_update_XXXX_email
+#  - generate_delete_XXXX_email
+#  - generate_email_footer
+#
+# Note also that this function cannot 'exit' from the script; when this
+# function is running (in hook script mode), the send_mail() function
+# is already executing in another process, connected via a pipe, and
+# if this function exits without, whatever has been generated to that
+# point will be sent as an email... even if nothing has been generated.
+#
+generate_email()
+{
+	# Email parameters
+	# The email subject will contain the best description of the ref
+	# that we can build from the parameters
+	describe=$(git describe $rev 2>/dev/null)
+	if [ -z "$describe" ]; then
+		describe=$rev
+	fi
+
+	generate_email_header
+
+	# Call the correct body generation function
+	fn_name=general
+	case "$refname_type" in
+	"tracking branch"|branch)
+		fn_name=branch
+		;;
+	"annotated tag")
+		fn_name=atag
+		;;
+	esac
+
+	if [ -z "$maxlines" ]; then
+		generate_${change_type}_${fn_name}_email
+	else
+		generate_${change_type}_${fn_name}_email | limit_lines $maxlines
+	fi
+
+	generate_email_footer
+}
+
+generate_email_header()
+{
+	# --- Email (all stdout will be the email)
+	# Generate header
+	cat <<-EOF
+	To: $recipients
+	Subject: ${emailprefix}$projectdesc $refname_type $short_refname ${change_type}d. $describe
+	MIME-Version: 1.0
+	Content-Type: text/plain; charset=utf-8
+	Content-Transfer-Encoding: 8bit
+	X-Git-Refname: $refname
+	X-Git-Reftype: $refname_type
+	X-Git-Oldrev: $oldrev
+	X-Git-Newrev: $newrev
+	Auto-Submitted: auto-generated
+
+	This is an automated email from the git hooks/post-receive script. It was
+	generated because a ref change was pushed to the repository containing
+	the project "$projectdesc".
+
+	The $refname_type, $short_refname has been ${change_type}d
+	EOF
+}
+
+generate_email_footer()
+{
+	SPACE=" "
+	cat <<-EOF
+
+
+	hooks/post-receive
+	--${SPACE}
+	$projectdesc
+	EOF
+}
+
+# --------------- Branches
+
+#
+# Called for the creation of a branch
+#
+generate_create_branch_email()
+{
+	# This is a new branch and so oldrev is not valid
+	echo "        at  $newrev ($newrev_type)"
+	echo ""
+
+	echo $LOGBEGIN
+	show_new_revisions
+	echo $LOGEND
+}
+
+#
+# Called for the change of a pre-existing branch
+#
+generate_update_branch_email()
+{
+	# Consider this:
+	#   1 --- 2 --- O --- X --- 3 --- 4 --- N
+	#
+	# O is $oldrev for $refname
+	# N is $newrev for $refname
+	# X is a revision pointed to by some other ref, for which we may
+	#   assume that an email has already been generated.
+	# In this case we want to issue an email containing only revisions
+	# 3, 4, and N.  Given (almost) by
+	#
+	#  git rev-list N ^O --not --all
+	#
+	# The reason for the "almost", is that the "--not --all" will take
+	# precedence over the "N", and effectively will translate to
+	#
+	#  git rev-list N ^O ^X ^N
+	#
+	# So, we need to build up the list more carefully.  git rev-parse
+	# will generate a list of revs that may be fed into git rev-list.
+	# We can get it to make the "--not --all" part and then filter out
+	# the "^N" with:
+	#
+	#  git rev-parse --not --all | grep -v N
+	#
+	# Then, using the --stdin switch to git rev-list we have effectively
+	# manufactured
+	#
+	#  git rev-list N ^O ^X
+	#
+	# This leaves a problem when someone else updates the repository
+	# while this script is running.  Their new value of the ref we're
+	# working on would be included in the "--not --all" output; and as
+	# our $newrev would be an ancestor of that commit, it would exclude
+	# all of our commits.  What we really want is to exclude the current
+	# value of $refname from the --not list, rather than N itself.  So:
+	#
+	#  git rev-parse --not --all | grep -v $(git rev-parse $refname)
+	#
+	# Gets us to something pretty safe (apart from the small time
+	# between refname being read, and git rev-parse running - for that,
+	# I give up)
+	#
+	#
+	# Next problem, consider this:
+	#   * --- B --- * --- O ($oldrev)
+	#          \
+	#           * --- X --- * --- N ($newrev)
+	#
+	# That is to say, there is no guarantee that oldrev is a strict
+	# subset of newrev (it would have required a --force, but that's
+	# allowed).  So, we can't simply say rev-list $oldrev..$newrev.
+	# Instead we find the common base of the two revs and list from
+	# there.
+	#
+	# As above, we need to take into account the presence of X; if
+	# another branch is already in the repository and points at some of
+	# the revisions that we are about to output - we don't want them.
+	# The solution is as before: git rev-parse output filtered.
+	#
+	# Finally, tags: 1 --- 2 --- O --- T --- 3 --- 4 --- N
+	#
+	# Tags pushed into the repository generate nice shortlog emails that
+	# summarise the commits between them and the previous tag.  However,
+	# those emails don't include the full commit messages that we output
+	# for a branch update.  Therefore we still want to output revisions
+	# that have been output on a tag email.
+	#
+	# Luckily, git rev-parse includes just the tool.  Instead of using
+	# "--all" we use "--branches"; this has the added benefit that
+	# "remotes/" will be ignored as well.
+
+	# List all of the revisions that were removed by this update, in a
+	# fast-forward update, this list will be empty, because rev-list O
+	# ^N is empty.  For a non-fast-forward, O ^N is the list of removed
+	# revisions
+	fast_forward=""
+	rev=""
+	for rev in $(git rev-list $newrev..$oldrev)
+	do
+		revtype=$(git cat-file -t "$rev")
+		echo "  discards  $rev ($revtype)"
+	done
+	if [ -z "$rev" ]; then
+		fast_forward=1
+	fi
+
+	# List all the revisions from baserev to newrev in a kind of
+	# "table-of-contents"; note this list can include revisions that
+	# have already had notification emails and is present to show the
+	# full detail of the change from rolling back the old revision to
+	# the base revision and then forward to the new revision
+	for rev in $(git rev-list $oldrev..$newrev)
+	do
+		revtype=$(git cat-file -t "$rev")
+		echo "       via  $rev ($revtype)"
+	done
+
+	if [ "$fast_forward" ]; then
+		echo "      from  $oldrev ($oldrev_type)"
+	else
+		#  1. Existing revisions were removed.  In this case newrev
+		#     is a subset of oldrev - this is the reverse of a
+		#     fast-forward, a rewind
+		#  2. New revisions were added on top of an old revision,
+		#     this is a rewind and addition.
+
+		# (1) certainly happened, (2) possibly.  When (2) hasn't
+		# happened, we set a flag to indicate that no log printout
+		# is required.
+
+		echo ""
+
+		# Find the common ancestor of the old and new revisions and
+		# compare it with newrev
+		baserev=$(git merge-base $oldrev $newrev)
+		rewind_only=""
+		if [ "$baserev" = "$newrev" ]; then
+			echo "This update discarded existing revisions and left the branch pointing at"
+			echo "a previous point in the repository history."
+			echo ""
+			echo " * -- * -- N ($newrev)"
+			echo "            \\"
+			echo "             O -- O -- O ($oldrev)"
+			echo ""
+			echo "The removed revisions are not necessarily gone - if another reference"
+			echo "still refers to them they will stay in the repository."
+			rewind_only=1
+		else
+			echo "This update added new revisions after undoing existing revisions.  That is"
+			echo "to say, the old revision is not a strict subset of the new revision.  This"
+			echo "situation occurs when you --force push a change and generate a repository"
+			echo "containing something like this:"
+			echo ""
+			echo " * -- * -- B -- O -- O -- O ($oldrev)"
+			echo "            \\"
+			echo "             N -- N -- N ($newrev)"
+			echo ""
+			echo "When this happens we assume that you've already had alert emails for all"
+			echo "of the O revisions, and so we here report only the revisions in the N"
+			echo "branch from the common base, B."
+		fi
+	fi
+
+	echo ""
+	if [ -z "$rewind_only" ]; then
+		echo "Those revisions listed above that are new to this repository have"
+		echo "not appeared on any other notification email; so we list those"
+		echo "revisions in full, below."
+
+		echo ""
+		echo $LOGBEGIN
+		show_new_revisions
+
+		# XXX: Need a way of detecting whether git rev-list actually
+		# outputted anything, so that we can issue a "no new
+		# revisions added by this update" message
+
+		echo $LOGEND
+	else
+		echo "No new revisions were added by this update."
+	fi
+
+	# The diffstat is shown from the old revision to the new revision.
+	# This is to show the truth of what happened in this change.
+	# There's no point showing the stat from the base to the new
+	# revision because the base is effectively a random revision at this
+	# point - the user will be interested in what this revision changed
+	# - including the undoing of previous revisions in the case of
+	# non-fast-forward updates.
+	echo ""
+	echo "Summary of changes:"
+	git diff-tree $diffopts $oldrev..$newrev
+}
+
+#
+# Called for the deletion of a branch
+#
+generate_delete_branch_email()
+{
+	echo "       was  $oldrev"
+	echo ""
+	echo $LOGBEGIN
+	git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev
+	echo $LOGEND
+}
+
+# --------------- Annotated tags
+
+#
+# Called for the creation of an annotated tag
+#
+generate_create_atag_email()
+{
+	echo "        at  $newrev ($newrev_type)"
+
+	generate_atag_email
+}
+
+#
+# Called for the update of an annotated tag (this is probably a rare event
+# and may not even be allowed)
+#
+generate_update_atag_email()
+{
+	echo "        to  $newrev ($newrev_type)"
+	echo "      from  $oldrev (which is now obsolete)"
+
+	generate_atag_email
+}
+
+#
+# Called when an annotated tag is created or changed
+#
+generate_atag_email()
+{
+	# Use git for-each-ref to pull out the individual fields from the
+	# tag
+	eval $(git for-each-ref --shell --format='
+	tagobject=%(*objectname)
+	tagtype=%(*objecttype)
+	tagger=%(taggername)
+	tagged=%(taggerdate)' $refname
+	)
+
+	echo "   tagging  $tagobject ($tagtype)"
+	case "$tagtype" in
+	commit)
+
+		# If the tagged object is a commit, then we assume this is a
+		# release, and so we calculate which tag this tag is
+		# replacing
+		prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null)
+
+		if [ -n "$prevtag" ]; then
+			echo "  replaces  $prevtag"
+		fi
+		;;
+	*)
+		echo "    length  $(git cat-file -s $tagobject) bytes"
+		;;
+	esac
+	echo " tagged by  $tagger"
+	echo "        on  $tagged"
+
+	echo ""
+	echo $LOGBEGIN
+
+	# Show the content of the tag message; this might contain a change
+	# log or release notes so is worth displaying.
+	git cat-file tag $newrev | sed -e '1,/^$/d'
+
+	echo ""
+	case "$tagtype" in
+	commit)
+		# Only commit tags make sense to have rev-list operations
+		# performed on them
+		if [ -n "$prevtag" ]; then
+			# Show changes since the previous release
+			git shortlog "$prevtag..$newrev"
+		else
+			# No previous tag, show all the changes since time
+			# began
+			git shortlog $newrev
+		fi
+		;;
+	*)
+		# XXX: Is there anything useful we can do for non-commit
+		# objects?
+		;;
+	esac
+
+	echo $LOGEND
+}
+
+#
+# Called for the deletion of an annotated tag
+#
+generate_delete_atag_email()
+{
+	echo "       was  $oldrev"
+	echo ""
+	echo $LOGBEGIN
+	git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev
+	echo $LOGEND
+}
+
+# --------------- General references
+
+#
+# Called when any other type of reference is created (most likely a
+# non-annotated tag)
+#
+generate_create_general_email()
+{
+	echo "        at  $newrev ($newrev_type)"
+
+	generate_general_email
+}
+
+#
+# Called when any other type of reference is updated (most likely a
+# non-annotated tag)
+#
+generate_update_general_email()
+{
+	echo "        to  $newrev ($newrev_type)"
+	echo "      from  $oldrev"
+
+	generate_general_email
+}
+
+#
+# Called for creation or update of any other type of reference
+#
+generate_general_email()
+{
+	# Unannotated tags are more about marking a point than releasing a
+	# version; therefore we don't do the shortlog summary that we do for
+	# annotated tags above - we simply show that the point has been
+	# marked, and print the log message for the marked point for
+	# reference purposes
+	#
+	# Note this section also catches any other reference type (although
+	# there aren't any) and deals with them in the same way.
+
+	echo ""
+	if [ "$newrev_type" = "commit" ]; then
+		echo $LOGBEGIN
+		git diff-tree -s --always --encoding=UTF-8 --pretty=medium $newrev
+		echo $LOGEND
+	else
+		# What can we do here?  The tag marks an object that is not
+		# a commit, so there is no log for us to display.  It's
+		# probably not wise to output git cat-file as it could be a
+		# binary blob.  We'll just say how big it is
+		echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long."
+	fi
+}
+
+#
+# Called for the deletion of any other type of reference
+#
+generate_delete_general_email()
+{
+	echo "       was  $oldrev"
+	echo ""
+	echo $LOGBEGIN
+	git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev
+	echo $LOGEND
+}
+
+
+# --------------- Miscellaneous utilities
+
+#
+# Show new revisions as the user would like to see them in the email.
+#
+show_new_revisions()
+{
+	# This shows all log entries that are not already covered by
+	# another ref - i.e. commits that are now accessible from this
+	# ref that were previously not accessible
+	# (see generate_update_branch_email for the explanation of this
+	# command)
+
+	# Revision range passed to rev-list differs for new vs. updated
+	# branches.
+	if [ "$change_type" = create ]
+	then
+		# Show all revisions exclusive to this (new) branch.
+		revspec=$newrev
+	else
+		# Branch update; show revisions not part of $oldrev.
+		revspec=$oldrev..$newrev
+	fi
+
+	other_branches=$(git for-each-ref --format='%(refname)' refs/heads/ |
+	    grep -F -v $refname)
+	git rev-parse --not $other_branches |
+	if [ -z "$custom_showrev" ]
+	then
+		git rev-list --pretty --stdin $revspec
+	else
+		git rev-list --stdin $revspec |
+		while read onerev
+		do
+			eval $(printf "$custom_showrev" $onerev)
+		done
+	fi
+}
+
+
+limit_lines()
+{
+	lines=0
+	skipped=0
+	while IFS="" read -r line; do
+		lines=$((lines + 1))
+		if [ $lines -gt $1 ]; then
+			skipped=$((skipped + 1))
+		else
+			printf "%s\n" "$line"
+		fi
+	done
+	if [ $skipped -ne 0 ]; then
+		echo "... $skipped lines suppressed ..."
+	fi
+}
+
+
+send_mail()
+{
+	if [ -n "$envelopesender" ]; then
+		/usr/sbin/sendmail -t -f "$envelopesender"
+	else
+		/usr/sbin/sendmail -t
+	fi
+}
+
+# ---------------------------- main()
+
+# --- Constants
+LOGBEGIN="- Log -----------------------------------------------------------------"
+LOGEND="-----------------------------------------------------------------------"
+
+# --- Config
+# Set GIT_DIR either from the working directory, or from the environment
+# variable.
+GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
+if [ -z "$GIT_DIR" ]; then
+	echo >&2 "fatal: post-receive: GIT_DIR not set"
+	exit 1
+fi
+
+projectdesc=$(sed -ne '1p' "$GIT_DIR/description" 2>/dev/null)
+# Check if the description is unchanged from it's default, and shorten it to
+# a more manageable length if it is
+if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null
+then
+	projectdesc="UNNAMED PROJECT"
+fi
+
+recipients=$(git config hooks.mailinglist)
+announcerecipients=$(git config hooks.announcelist)
+envelopesender=$(git config hooks.envelopesender)
+emailprefix=$(git config hooks.emailprefix || echo '[SCM] ')
+custom_showrev=$(git config hooks.showrev)
+maxlines=$(git config hooks.emailmaxlines)
+diffopts=$(git config hooks.diffopts)
+: ${diffopts:="--stat --summary --find-copies-harder"}
+
+# --- Main loop
+# Allow dual mode: run from the command line just like the update hook, or
+# if no arguments are given then run as a hook script
+if [ -n "$1" -a -n "$2" -a -n "$3" ]; then
+	# Output to the terminal in command line mode - if someone wanted to
+	# resend an email; they could redirect the output to sendmail
+	# themselves
+	prep_for_email $2 $3 $1 && PAGER= generate_email
+else
+	while read oldrev newrev refname
+	do
+		prep_for_email $oldrev $newrev $refname || continue
+		generate_email $maxlines | send_mail
+	done
+fi
blob - /dev/null
blob + ec17ec1939b7c3e86b7cb6c0c4de6b0818a7e75e (mode 755)
--- /dev/null
+++ .git/hooks/post-update.sample
@@ -0,0 +1,8 @@
+#!/bin/sh
+#
+# An example hook script to prepare a packed repository for use over
+# dumb transports.
+#
+# To enable this hook, rename this file to "post-update".
+
+exec git update-server-info
blob - /dev/null
blob + 4142082bcb939bbc17985a69ba748491ac6b62a5 (mode 755)
--- /dev/null
+++ .git/hooks/pre-applypatch.sample
@@ -0,0 +1,14 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed
+# by applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-applypatch".
+
+. git-sh-setup
+precommit="$(git rev-parse --git-path hooks/pre-commit)"
+test -x "$precommit" && exec "$precommit" ${1+"$@"}
+:
blob - /dev/null
blob + 29ed5ee486a4f07c3f0558101ef8efc46f3d6ab7 (mode 755)
--- /dev/null
+++ .git/hooks/pre-commit.sample
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by "git commit" with no arguments.  The hook should
+# exit with non-zero status after issuing an appropriate message if
+# it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-commit".
+
+if git rev-parse --verify HEAD >/dev/null 2>&1
+then
+	against=HEAD
+else
+	# Initial commit: diff against an empty tree object
+	against=$(git hash-object -t tree /dev/null)
+fi
+
+# If you want to allow non-ASCII filenames set this variable to true.
+allownonascii=$(git config --type=bool hooks.allownonascii)
+
+# Redirect output to stderr.
+exec 1>&2
+
+# Cross platform projects tend to avoid non-ASCII filenames; prevent
+# them from being added to the repository. We exploit the fact that the
+# printable range starts at the space character and ends with tilde.
+if [ "$allownonascii" != "true" ] &&
+	# Note that the use of brackets around a tr range is ok here, (it's
+	# even required, for portability to Solaris 10's /usr/bin/tr), since
+	# the square bracket bytes happen to fall in the designated range.
+	test $(git diff-index --cached --name-only --diff-filter=A -z $against |
+	  LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
+then
+	cat <<\EOF
+Error: Attempt to add a non-ASCII file name.
+
+This can cause problems if you want to work with people on other platforms.
+
+To be portable it is advisable to rename the file.
+
+If you know what you are doing you can disable this check using:
+
+  git config hooks.allownonascii true
+EOF
+	exit 1
+fi
+
+# If there are whitespace errors, print the offending file names and fail.
+exec git diff-index --check --cached $against --
blob - /dev/null
blob + 399eab1924e39da570b389b0bef1ca713b3b05c3 (mode 755)
--- /dev/null
+++ .git/hooks/pre-merge-commit.sample
@@ -0,0 +1,13 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by "git merge" with no arguments.  The hook should
+# exit with non-zero status after issuing an appropriate message to
+# stderr if it wants to stop the merge commit.
+#
+# To enable this hook, rename this file to "pre-merge-commit".
+
+. git-sh-setup
+test -x "$GIT_DIR/hooks/pre-commit" &&
+        exec "$GIT_DIR/hooks/pre-commit"
+:
blob - /dev/null
blob + 4ce688d32b7532862767345f2b991ae856f7d4a8 (mode 755)
--- /dev/null
+++ .git/hooks/pre-push.sample
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+# An example hook script to verify what is about to be pushed.  Called by "git
+# push" after it has checked the remote status, but before anything has been
+# pushed.  If this script exits with a non-zero status nothing will be pushed.
+#
+# This hook is called with the following parameters:
+#
+# $1 -- Name of the remote to which the push is being done
+# $2 -- URL to which the push is being done
+#
+# If pushing without using a named remote those arguments will be equal.
+#
+# Information about the commits which are being pushed is supplied as lines to
+# the standard input in the form:
+#
+#   <local ref> <local oid> <remote ref> <remote oid>
+#
+# This sample shows how to prevent push of commits where the log message starts
+# with "WIP" (work in progress).
+
+remote="$1"
+url="$2"
+
+zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
+
+while read local_ref local_oid remote_ref remote_oid
+do
+	if test "$local_oid" = "$zero"
+	then
+		# Handle delete
+		:
+	else
+		if test "$remote_oid" = "$zero"
+		then
+			# New branch, examine all commits
+			range="$local_oid"
+		else
+			# Update to existing branch, examine new commits
+			range="$remote_oid..$local_oid"
+		fi
+
+		# Check for WIP commit
+		commit=$(git rev-list -n 1 --grep '^WIP' "$range")
+		if test -n "$commit"
+		then
+			echo >&2 "Found WIP commit in $local_ref, not pushing"
+			exit 1
+		fi
+	fi
+done
+
+exit 0
blob - /dev/null
blob + 6cbef5c370d8c3486ca85423dd70440c5e0a2aa2 (mode 755)
--- /dev/null
+++ .git/hooks/pre-rebase.sample
@@ -0,0 +1,169 @@
+#!/bin/sh
+#
+# Copyright (c) 2006, 2008 Junio C Hamano
+#
+# The "pre-rebase" hook is run just before "git rebase" starts doing
+# its job, and can prevent the command from running by exiting with
+# non-zero status.
+#
+# The hook is called with the following parameters:
+#
+# $1 -- the upstream the series was forked from.
+# $2 -- the branch being rebased (or empty when rebasing the current branch).
+#
+# This sample shows how to prevent topic branches that are already
+# merged to 'next' branch from getting rebased, because allowing it
+# would result in rebasing already published history.
+
+publish=next
+basebranch="$1"
+if test "$#" = 2
+then
+	topic="refs/heads/$2"
+else
+	topic=`git symbolic-ref HEAD` ||
+	exit 0 ;# we do not interrupt rebasing detached HEAD
+fi
+
+case "$topic" in
+refs/heads/??/*)
+	;;
+*)
+	exit 0 ;# we do not interrupt others.
+	;;
+esac
+
+# Now we are dealing with a topic branch being rebased
+# on top of master.  Is it OK to rebase it?
+
+# Does the topic really exist?
+git show-ref -q "$topic" || {
+	echo >&2 "No such branch $topic"
+	exit 1
+}
+
+# Is topic fully merged to master?
+not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
+if test -z "$not_in_master"
+then
+	echo >&2 "$topic is fully merged to master; better remove it."
+	exit 1 ;# we could allow it, but there is no point.
+fi
+
+# Is topic ever merged to next?  If so you should not be rebasing it.
+only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
+only_next_2=`git rev-list ^master           ${publish} | sort`
+if test "$only_next_1" = "$only_next_2"
+then
+	not_in_topic=`git rev-list "^$topic" master`
+	if test -z "$not_in_topic"
+	then
+		echo >&2 "$topic is already up to date with master"
+		exit 1 ;# we could allow it, but there is no point.
+	else
+		exit 0
+	fi
+else
+	not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
+	/usr/bin/perl -e '
+		my $topic = $ARGV[0];
+		my $msg = "* $topic has commits already merged to public branch:\n";
+		my (%not_in_next) = map {
+			/^([0-9a-f]+) /;
+			($1 => 1);
+		} split(/\n/, $ARGV[1]);
+		for my $elem (map {
+				/^([0-9a-f]+) (.*)$/;
+				[$1 => $2];
+			} split(/\n/, $ARGV[2])) {
+			if (!exists $not_in_next{$elem->[0]}) {
+				if ($msg) {
+					print STDERR $msg;
+					undef $msg;
+				}
+				print STDERR " $elem->[1]\n";
+			}
+		}
+	' "$topic" "$not_in_next" "$not_in_master"
+	exit 1
+fi
+
+<<\DOC_END
+
+This sample hook safeguards topic branches that have been
+published from being rewound.
+
+The workflow assumed here is:
+
+ * Once a topic branch forks from "master", "master" is never
+   merged into it again (either directly or indirectly).
+
+ * Once a topic branch is fully cooked and merged into "master",
+   it is deleted.  If you need to build on top of it to correct
+   earlier mistakes, a new topic branch is created by forking at
+   the tip of the "master".  This is not strictly necessary, but
+   it makes it easier to keep your history simple.
+
+ * Whenever you need to test or publish your changes to topic
+   branches, merge them into "next" branch.
+
+The script, being an example, hardcodes the publish branch name
+to be "next", but it is trivial to make it configurable via
+$GIT_DIR/config mechanism.
+
+With this workflow, you would want to know:
+
+(1) ... if a topic branch has ever been merged to "next".  Young
+    topic branches can have stupid mistakes you would rather
+    clean up before publishing, and things that have not been
+    merged into other branches can be easily rebased without
+    affecting other people.  But once it is published, you would
+    not want to rewind it.
+
+(2) ... if a topic branch has been fully merged to "master".
+    Then you can delete it.  More importantly, you should not
+    build on top of it -- other people may already want to
+    change things related to the topic as patches against your
+    "master", so if you need further changes, it is better to
+    fork the topic (perhaps with the same name) afresh from the
+    tip of "master".
+
+Let's look at this example:
+
+		   o---o---o---o---o---o---o---o---o---o "next"
+		  /       /           /           /
+		 /   a---a---b A     /           /
+		/   /               /           /
+	       /   /   c---c---c---c B         /
+	      /   /   /             \         /
+	     /   /   /   b---b C     \       /
+	    /   /   /   /             \     /
+    ---o---o---o---o---o---o---o---o---o---o---o "master"
+
+
+A, B and C are topic branches.
+
+ * A has one fix since it was merged up to "next".
+
+ * B has finished.  It has been fully merged up to "master" and "next",
+   and is ready to be deleted.
+
+ * C has not merged to "next" at all.
+
+We would want to allow C to be rebased, refuse A, and encourage
+B to be deleted.
+
+To compute (1):
+
+	git rev-list ^master ^topic next
+	git rev-list ^master        next
+
+	if these match, topic has not merged in next at all.
+
+To compute (2):
+
+	git rev-list master..topic
+
+	if this is empty, it is fully merged to "master".
+
+DOC_END
blob - /dev/null
blob + a1fd29ec14823d8bc4a8d1a2cfe35451580f5118 (mode 755)
--- /dev/null
+++ .git/hooks/pre-receive.sample
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# An example hook script to make use of push options.
+# The example simply echoes all push options that start with 'echoback='
+# and rejects all pushes when the "reject" push option is used.
+#
+# To enable this hook, rename this file to "pre-receive".
+
+if test -n "$GIT_PUSH_OPTION_COUNT"
+then
+	i=0
+	while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
+	do
+		eval "value=\$GIT_PUSH_OPTION_$i"
+		case "$value" in
+		echoback=*)
+			echo "echo from the pre-receive-hook: ${value#*=}" >&2
+			;;
+		reject)
+			exit 1
+		esac
+		i=$((i + 1))
+	done
+fi
blob - /dev/null
blob + 10fa14c5ab0134436e2ae435138bf921eb477c60 (mode 755)
--- /dev/null
+++ .git/hooks/prepare-commit-msg.sample
@@ -0,0 +1,42 @@
+#!/bin/sh
+#
+# An example hook script to prepare the commit log message.
+# Called by "git commit" with the name of the file that has the
+# commit message, followed by the description of the commit
+# message's source.  The hook's purpose is to edit the commit
+# message file.  If the hook fails with a non-zero status,
+# the commit is aborted.
+#
+# To enable this hook, rename this file to "prepare-commit-msg".
+
+# This hook includes three examples. The first one removes the
+# "# Please enter the commit message..." help message.
+#
+# The second includes the output of "git diff --name-status -r"
+# into the message, just before the "git status" output.  It is
+# commented because it doesn't cope with --amend or with squashed
+# commits.
+#
+# The third example adds a Signed-off-by line to the message, that can
+# still be edited.  This is rarely a good idea.
+
+COMMIT_MSG_FILE=$1
+COMMIT_SOURCE=$2
+SHA1=$3
+
+/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
+
+# case "$COMMIT_SOURCE,$SHA1" in
+#  ,|template,)
+#    /usr/bin/perl -i.bak -pe '
+#       print "\n" . `git diff --cached --name-status -r`
+# 	 if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
+#  *) ;;
+# esac
+
+# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
+# if test -z "$COMMIT_SOURCE"
+# then
+#   /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
+# fi
blob - /dev/null
blob + af5a0c0018b5e9c04b56ac52f21b4d28f48d99ea (mode 755)
--- /dev/null
+++ .git/hooks/push-to-checkout.sample
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+# An example hook script to update a checked-out tree on a git push.
+#
+# This hook is invoked by git-receive-pack(1) when it reacts to git
+# push and updates reference(s) in its repository, and when the push
+# tries to update the branch that is currently checked out and the
+# receive.denyCurrentBranch configuration variable is set to
+# updateInstead.
+#
+# By default, such a push is refused if the working tree and the index
+# of the remote repository has any difference from the currently
+# checked out commit; when both the working tree and the index match
+# the current commit, they are updated to match the newly pushed tip
+# of the branch. This hook is to be used to override the default
+# behaviour; however the code below reimplements the default behaviour
+# as a starting point for convenient modification.
+#
+# The hook receives the commit with which the tip of the current
+# branch is going to be updated:
+commit=$1
+
+# It can exit with a non-zero status to refuse the push (when it does
+# so, it must not modify the index or the working tree).
+die () {
+	echo >&2 "$*"
+	exit 1
+}
+
+# Or it can make any necessary changes to the working tree and to the
+# index to bring them to the desired state when the tip of the current
+# branch is updated to the new commit, and exit with a zero status.
+#
+# For example, the hook can simply run git read-tree -u -m HEAD "$1"
+# in order to emulate git fetch that is run in the reverse direction
+# with git push, as the two-tree form of git read-tree -u -m is
+# essentially the same as git switch or git checkout that switches
+# branches while keeping the local changes in the working tree that do
+# not interfere with the difference between the branches.
+
+# The below is a more-or-less exact translation to shell of the C code
+# for the default behaviour for git's push-to-checkout hook defined in
+# the push_to_deploy() function in builtin/receive-pack.c.
+#
+# Note that the hook will be executed from the repository directory,
+# not from the working tree, so if you want to perform operations on
+# the working tree, you will have to adapt your code accordingly, e.g.
+# by adding "cd .." or using relative paths.
+
+if ! git update-index -q --ignore-submodules --refresh
+then
+	die "Up-to-date check failed"
+fi
+
+if ! git diff-files --quiet --ignore-submodules --
+then
+	die "Working directory has unstaged changes"
+fi
+
+# This is a rough translation of:
+#
+#   head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
+if git cat-file -e HEAD 2>/dev/null
+then
+	head=HEAD
+else
+	head=$(git hash-object -t tree --stdin </dev/null)
+fi
+
+if ! git diff-index --quiet --cached --ignore-submodules $head --
+then
+	die "Working directory has staged changes"
+fi
+
+if ! git read-tree -u -m "$commit"
+then
+	die "Could not update working tree to new HEAD"
+fi
blob - /dev/null
blob + 640bcf874dc0bef6d128d09ed4881f0616395ed8 (mode 755)
--- /dev/null
+++ .git/hooks/sendemail-validate.sample
@@ -0,0 +1,77 @@
+#!/bin/sh
+
+# An example hook script to validate a patch (and/or patch series) before
+# sending it via email.
+#
+# The hook should exit with non-zero status after issuing an appropriate
+# message if it wants to prevent the email(s) from being sent.
+#
+# To enable this hook, rename this file to "sendemail-validate".
+#
+# By default, it will only check that the patch(es) can be applied on top of
+# the default upstream branch without conflicts in a secondary worktree. After
+# validation (successful or not) of the last patch of a series, the worktree
+# will be deleted.
+#
+# The following config variables can be set to change the default remote and
+# remote ref that are used to apply the patches against:
+#
+#   sendemail.validateRemote (default: origin)
+#   sendemail.validateRemoteRef (default: HEAD)
+#
+# Replace the TODO placeholders with appropriate checks according to your
+# needs.
+
+validate_cover_letter () {
+	file="$1"
+	# TODO: Replace with appropriate checks (e.g. spell checking).
+	true
+}
+
+validate_patch () {
+	file="$1"
+	# Ensure that the patch applies without conflicts.
+	git am -3 "$file" || return
+	# TODO: Replace with appropriate checks for this patch
+	# (e.g. checkpatch.pl).
+	true
+}
+
+validate_series () {
+	# TODO: Replace with appropriate checks for the whole series
+	# (e.g. quick build, coding style checks, etc.).
+	true
+}
+
+# main -------------------------------------------------------------------------
+
+if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
+then
+	remote=$(git config --default origin --get sendemail.validateRemote) &&
+	ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
+	worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
+	git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
+	git config --replace-all sendemail.validateWorktree "$worktree"
+else
+	worktree=$(git config --get sendemail.validateWorktree)
+fi || {
+	echo "sendemail-validate: error: failed to prepare worktree" >&2
+	exit 1
+}
+
+unset GIT_DIR GIT_WORK_TREE
+cd "$worktree" &&
+
+if grep -q "^diff --git " "$1"
+then
+	validate_patch "$1"
+else
+	validate_cover_letter "$1"
+fi &&
+
+if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
+then
+	git config --unset-all sendemail.validateWorktree &&
+	trap 'git worktree remove -ff "$worktree"' EXIT &&
+	validate_series
+fi
blob - /dev/null
blob + 2770a1b1d205ee6b6edaec291a7dce3fc417027f (mode 644)
--- /dev/null
+++ .git/hooks/setgitperms.perl
@@ -0,0 +1,214 @@
+#!/usr/bin/perl
+#
+# Copyright (c) 2006 Josh England
+#
+# This script can be used to save/restore full permissions and ownership data
+# within a git working tree.
+#
+# To save permissions/ownership data, place this script in your .git/hooks
+# directory and enable a `pre-commit` hook with the following lines:
+#      #!/bin/sh
+#     SUBDIRECTORY_OK=1 . git-sh-setup
+#     $GIT_DIR/hooks/setgitperms.perl -r
+#
+# To restore permissions/ownership data, place this script in your .git/hooks
+# directory and enable a `post-merge` and `post-checkout` hook with the
+# following lines:
+#      #!/bin/sh
+#     SUBDIRECTORY_OK=1 . git-sh-setup
+#     $GIT_DIR/hooks/setgitperms.perl -w
+#
+use strict;
+use Getopt::Long;
+use File::Find;
+use File::Basename;
+
+my $usage =
+"usage: setgitperms.perl [OPTION]... <--read|--write>
+This program uses a file `.gitmeta` to store/restore permissions and uid/gid
+info for all files/dirs tracked by git in the repository.
+
+---------------------------------Read Mode-------------------------------------
+-r,  --read         Reads perms/etc from working dir into a .gitmeta file
+-s,  --stdout       Output to stdout instead of .gitmeta
+-d,  --diff         Show unified diff of perms file (XOR with --stdout)
+
+---------------------------------Write Mode------------------------------------
+-w,  --write        Modify perms/etc in working dir to match the .gitmeta file
+-v,  --verbose      Be verbose
+
+\n";
+
+my ($stdout, $showdiff, $verbose, $read_mode, $write_mode);
+
+if ((@ARGV < 0) || !GetOptions(
+			       "stdout",         \$stdout,
+			       "diff",           \$showdiff,
+			       "read",           \$read_mode,
+			       "write",          \$write_mode,
+			       "verbose",        \$verbose,
+			      )) { die $usage; }
+die $usage unless ($read_mode xor $write_mode);
+
+my $topdir = `git rev-parse --show-cdup` or die "\n"; chomp $topdir;
+my $gitdir = $topdir . '.git';
+my $gitmeta = $topdir . '.gitmeta';
+
+if ($write_mode) {
+    # Update the working dir permissions/ownership based on data from .gitmeta
+    open (IN, "<$gitmeta") or die "Could not open $gitmeta for reading: $!\n";
+    while (defined ($_ = <IN>)) {
+	chomp;
+	if (/^(.*)  mode=(\S+)\s+uid=(\d+)\s+gid=(\d+)/) {
+	    # Compare recorded perms to actual perms in the working dir
+	    my ($path, $mode, $uid, $gid) = ($1, $2, $3, $4);
+	    my $fullpath = $topdir . $path;
+	    my (undef,undef,$wmode,undef,$wuid,$wgid) = lstat($fullpath);
+	    $wmode = sprintf "%04o", $wmode & 07777;
+	    if ($mode ne $wmode) {
+		$verbose && print "Updating permissions on $path: old=$wmode, new=$mode\n";
+		chmod oct($mode), $fullpath;
+	    }
+	    if ($uid != $wuid || $gid != $wgid) {
+		if ($verbose) {
+		    # Print out user/group names instead of uid/gid
+		    my $pwname  = getpwuid($uid);
+		    my $grpname  = getgrgid($gid);
+		    my $wpwname  = getpwuid($wuid);
+		    my $wgrpname  = getgrgid($wgid);
+		    $pwname = $uid if !defined $pwname;
+		    $grpname = $gid if !defined $grpname;
+		    $wpwname = $wuid if !defined $wpwname;
+		    $wgrpname = $wgid if !defined $wgrpname;
+
+		    print "Updating uid/gid on $path: old=$wpwname/$wgrpname, new=$pwname/$grpname\n";
+		}
+		chown $uid, $gid, $fullpath;
+	    }
+	}
+	else {
+	    warn "Invalid input format in $gitmeta:\n\t$_\n";
+	}
+    }
+    close IN;
+}
+elsif ($read_mode) {
+    # Handle merge conflicts in the .gitperms file
+    if (-e "$gitdir/MERGE_MSG") {
+	if (`grep ====== $gitmeta`) {
+	    # Conflict not resolved -- abort the commit
+	    print "PERMISSIONS/OWNERSHIP CONFLICT\n";
+	    print "    Resolve the conflict in the $gitmeta file and then run\n";
+	    print "    `.git/hooks/setgitperms.perl --write` to reconcile.\n";
+	    exit 1;
+	}
+	elsif (`grep $gitmeta $gitdir/MERGE_MSG`) {
+	    # A conflict in .gitmeta has been manually resolved. Verify that
+	    # the working dir perms matches the current .gitmeta perms for
+	    # each file/dir that conflicted.
+	    # This is here because a `setgitperms.perl --write` was not
+	    # performed due to a merge conflict, so permissions/ownership
+	    # may not be consistent with the manually merged .gitmeta file.
+	    my @conflict_diff = `git show \$(cat $gitdir/MERGE_HEAD)`;
+	    my @conflict_files;
+	    my $metadiff = 0;
+
+	    # Build a list of files that conflicted from the .gitmeta diff
+	    foreach my $line (@conflict_diff) {
+		if ($line =~ m|^diff --git a/$gitmeta b/$gitmeta|) {
+		    $metadiff = 1;
+		}
+		elsif ($line =~ /^diff --git/) {
+		    $metadiff = 0;
+		}
+		elsif ($metadiff && $line =~ /^\+(.*)  mode=/) {
+		    push @conflict_files, $1;
+		}
+	    }
+
+	    # Verify that each conflict file now has permissions consistent
+	    # with the .gitmeta file
+	    foreach my $file (@conflict_files) {
+		my $absfile = $topdir . $file;
+		my $gm_entry = `grep "^$file  mode=" $gitmeta`;
+		if ($gm_entry =~ /mode=(\d+)  uid=(\d+)  gid=(\d+)/) {
+		    my ($gm_mode, $gm_uid, $gm_gid) = ($1, $2, $3);
+		    my (undef,undef,$mode,undef,$uid,$gid) = lstat("$absfile");
+		    $mode = sprintf("%04o", $mode & 07777);
+		    if (($gm_mode ne $mode) || ($gm_uid != $uid)
+			|| ($gm_gid != $gid)) {
+			print "PERMISSIONS/OWNERSHIP CONFLICT\n";
+			print "    Mismatch found for file: $file\n";
+			print "    Run `.git/hooks/setgitperms.perl --write` to reconcile.\n";
+			exit 1;
+		    }
+		}
+		else {
+		    print "Warning! Permissions/ownership no longer being tracked for file: $file\n";
+		}
+	    }
+	}
+    }
+
+    # No merge conflicts -- write out perms/ownership data to .gitmeta file
+    unless ($stdout) {
+	open (OUT, ">$gitmeta.tmp") or die "Could not open $gitmeta.tmp for writing: $!\n";
+    }
+
+    my @files = `git ls-files`;
+    my %dirs;
+
+    foreach my $path (@files) {
+	chomp $path;
+	# We have to manually add stats for parent directories
+	my $parent = dirname($path);
+	while (!exists $dirs{$parent}) {
+	    $dirs{$parent} = 1;
+	    next if $parent eq '.';
+	    printstats($parent);
+	    $parent = dirname($parent);
+	}
+	# Now the git-tracked file
+	printstats($path);
+    }
+
+    # diff the temporary metadata file to see if anything has changed
+    # If no metadata has changed, don't overwrite the real file
+    # This is just so `git commit -a` doesn't try to commit a bogus update
+    unless ($stdout) {
+	if (! -e $gitmeta) {
+	    rename "$gitmeta.tmp", $gitmeta;
+	}
+	else {
+	    my $diff = `diff -U 0 $gitmeta $gitmeta.tmp`;
+	    if ($diff ne '') {
+		rename "$gitmeta.tmp", $gitmeta;
+	    }
+	    else {
+		unlink "$gitmeta.tmp";
+	    }
+	    if ($showdiff) {
+		print $diff;
+	    }
+	}
+	close OUT;
+    }
+    # Make sure the .gitmeta file is tracked
+    system("git add $gitmeta");
+}
+
+
+sub printstats {
+    my $path = $_[0];
+    $path =~ s/@/\@/g;
+    my (undef,undef,$mode,undef,$uid,$gid) = lstat($path);
+    $path =~ s/%/\%/g;
+    if ($stdout) {
+	print $path;
+	printf "  mode=%04o  uid=$uid  gid=$gid\n", $mode & 07777;
+    }
+    else {
+	print OUT $path;
+	printf OUT "  mode=%04o  uid=$uid  gid=$gid\n", $mode & 07777;
+    }
+}
blob - /dev/null
blob + c4d426bc6ee9430ee7813263ce6d5da7ec78c3c6 (mode 755)
--- /dev/null
+++ .git/hooks/update.sample
@@ -0,0 +1,128 @@
+#!/bin/sh
+#
+# An example hook script to block unannotated tags from entering.
+# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
+#
+# To enable this hook, rename this file to "update".
+#
+# Config
+# ------
+# hooks.allowunannotated
+#   This boolean sets whether unannotated tags will be allowed into the
+#   repository.  By default they won't be.
+# hooks.allowdeletetag
+#   This boolean sets whether deleting tags will be allowed in the
+#   repository.  By default they won't be.
+# hooks.allowmodifytag
+#   This boolean sets whether a tag may be modified after creation. By default
+#   it won't be.
+# hooks.allowdeletebranch
+#   This boolean sets whether deleting branches will be allowed in the
+#   repository.  By default they won't be.
+# hooks.denycreatebranch
+#   This boolean sets whether remotely creating branches will be denied
+#   in the repository.  By default this is allowed.
+#
+
+# --- Command line
+refname="$1"
+oldrev="$2"
+newrev="$3"
+
+# --- Safety check
+if [ -z "$GIT_DIR" ]; then
+	echo "Don't run this script from the command line." >&2
+	echo " (if you want, you could supply GIT_DIR then run" >&2
+	echo "  $0 <ref> <oldrev> <newrev>)" >&2
+	exit 1
+fi
+
+if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
+	echo "usage: $0 <ref> <oldrev> <newrev>" >&2
+	exit 1
+fi
+
+# --- Config
+allowunannotated=$(git config --type=bool hooks.allowunannotated)
+allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
+denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
+allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
+allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
+
+# check for no description
+projectdesc=$(sed -e '1q' "$GIT_DIR/description")
+case "$projectdesc" in
+"Unnamed repository"* | "")
+	echo "*** Project description file hasn't been set" >&2
+	exit 1
+	;;
+esac
+
+# --- Check types
+# if $newrev is 0000...0000, it's a commit to delete a ref.
+zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
+if [ "$newrev" = "$zero" ]; then
+	newrev_type=delete
+else
+	newrev_type=$(git cat-file -t $newrev)
+fi
+
+case "$refname","$newrev_type" in
+	refs/tags/*,commit)
+		# un-annotated tag
+		short_refname=${refname##refs/tags/}
+		if [ "$allowunannotated" != "true" ]; then
+			echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
+			echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
+			exit 1
+		fi
+		;;
+	refs/tags/*,delete)
+		# delete tag
+		if [ "$allowdeletetag" != "true" ]; then
+			echo "*** Deleting a tag is not allowed in this repository" >&2
+			exit 1
+		fi
+		;;
+	refs/tags/*,tag)
+		# annotated tag
+		if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
+		then
+			echo "*** Tag '$refname' already exists." >&2
+			echo "*** Modifying a tag is not allowed in this repository." >&2
+			exit 1
+		fi
+		;;
+	refs/heads/*,commit)
+		# branch
+		if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
+			echo "*** Creating a branch is not allowed in this repository" >&2
+			exit 1
+		fi
+		;;
+	refs/heads/*,delete)
+		# delete branch
+		if [ "$allowdeletebranch" != "true" ]; then
+			echo "*** Deleting a branch is not allowed in this repository" >&2
+			exit 1
+		fi
+		;;
+	refs/remotes/*,commit)
+		# tracking branch
+		;;
+	refs/remotes/*,delete)
+		# delete tracking branch
+		if [ "$allowdeletebranch" != "true" ]; then
+			echo "*** Deleting a tracking branch is not allowed in this repository" >&2
+			exit 1
+		fi
+		;;
+	*)
+		# Anything else (is there anything else?)
+		echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
+		exit 1
+		;;
+esac
+
+# --- Finished
+exit 0
blob - /dev/null
blob + 834a9c06c236267a892f725288d9e5d3ffcf8b09 (mode 644)
Binary files /dev/null and .git/index differ
blob - /dev/null
blob + a5196d1be8fb59edf8062bef36d3a602e0812139 (mode 644)
--- /dev/null
+++ .git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
blob - /dev/null
blob + 761762c90550274cc3795e9db6b88f6140ee8f53 (mode 644)
--- /dev/null
+++ .git/logs/HEAD
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 7c65a52929d95ad5a4108706bb32c7c7f754b193 jrmu <jrmu@ircnow.org> 1735852998 -0800	clone: from git://ircforever.org/ticl
blob - /dev/null
blob + 761762c90550274cc3795e9db6b88f6140ee8f53 (mode 644)
--- /dev/null
+++ .git/logs/refs/heads/master
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 7c65a52929d95ad5a4108706bb32c7c7f754b193 jrmu <jrmu@ircnow.org> 1735852998 -0800	clone: from git://ircforever.org/ticl
blob - /dev/null
blob + 761762c90550274cc3795e9db6b88f6140ee8f53 (mode 644)
--- /dev/null
+++ .git/logs/refs/remotes/origin/HEAD
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 7c65a52929d95ad5a4108706bb32c7c7f754b193 jrmu <jrmu@ircnow.org> 1735852998 -0800	clone: from git://ircforever.org/ticl
blob - /dev/null
blob + c6ea055b424e6adf0736f8df12c43723f62f3d1a (mode 644)
Binary files /dev/null and .git/objects/pack/pack-f47c30a4fcb65a71c802bee4644d0b5a571e259c.idx differ
blob - /dev/null
blob + 24746d932a58b0ccdfa6c1cdbe128b345ec86002 (mode 644)
Binary files /dev/null and .git/objects/pack/pack-f47c30a4fcb65a71c802bee4644d0b5a571e259c.pack differ
blob - /dev/null
blob + 5e4195d3274f869b96a23cab34d91dabdf1bcee1 (mode 644)
Binary files /dev/null and .git/objects/pack/pack-f47c30a4fcb65a71c802bee4644d0b5a571e259c.rev differ
blob - /dev/null
blob + 674d26f93b3130b6f565228f77bb1847f1774e4f (mode 644)
--- /dev/null
+++ .git/packed-refs
@@ -0,0 +1,3 @@
+# pack-refs with: peeled fully-peeled sorted 
+7c65a52929d95ad5a4108706bb32c7c7f754b193 refs/remotes/origin/master
+3e87b9df1db2dd9449ab69a8be0df9008fe2024f refs/remotes/origin/test-bots
blob - /dev/null
blob + 06bf76fa1aa5d8ab09e9a4ae90e7f68d33d27752 (mode 644)
--- /dev/null
+++ .git/refs/heads/master
@@ -0,0 +1 @@
+7c65a52929d95ad5a4108706bb32c7c7f754b193
blob - /dev/null
blob + 6efe28fff834a94fbc20cac51fe9cd65ecc78d43 (mode 644)
--- /dev/null
+++ .git/refs/remotes/origin/HEAD
@@ -0,0 +1 @@
+ref: refs/remotes/origin/master
blob - /dev/null
blob + e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mode 644)
blob - /dev/null
blob + ca454d2f329d21edcfc675ca64415c794866c8bb (mode 644)
--- /dev/null
+++ COPYING
@@ -0,0 +1,13 @@
+The author(s) of this work have dedicated this work to the public domain by
+waiving all claims of copyright, patent and trademark rights to this work.
+
+This work is provided 'as-is', without any express or implied warranty.
+In no event shall the author(s) be liable for any damages arising from
+the use of this work.
+
+Alternatively, this work is available under any of the following licenses:
+
+Unlicense	https://spdx.org/licenses/Unlicense.html
+CC0-1.0		https://spdx.org/licenses/CC0-1.0.html
+MIT-0		https://spdx.org/licenses/MIT-0.html
+0BSD		https://spdx.org/licenses/0BSD.html
blob - /dev/null
blob + 280a19a2ba0ff6a90df20d4e84d39ed34329f6f2 (mode 644)
--- /dev/null
+++ Makefile
@@ -0,0 +1,37 @@
+# This work is dedicated to the public domain.
+# See COPYING file for more information.
+
+include config.mk
+
+OUT = ticl
+SRC = main.c htable.c
+OBJ = $(SRC:.c=.o)
+
+all: clean $(OUT)
+
+.c.o:
+	$(CC) -c $(CFLAGS) $<
+
+$(OUT): $(OBJ)
+	$(CC) -o $@ $(OBJ) $(LDFLAGS)
+
+clean:
+	rm -f $(OUT) $(OBJ) $(OUT)-*.tar.gz
+
+dist: clean
+	mkdir -p $(OUT)-$(VERSION)
+	cp -R README COPYING Makefile config.mk htable.h util.c $(SRC)\
+		$(OUT)-$(VERSION)
+	tar -cf	$(OUT)-$(VERSION).tar $(OUT)-$(VERSION)
+	gzip $(OUT)-$(VERSION).tar
+	rm -rf $(OUT)-$(VERSION)
+
+install: all
+	mkdir -p $(DESTDIR)$(PREFIX)/bin
+	cp -f $(OUT) $(DESTDIR)$(PREFIX)/bin
+	chmod 755 $(DESTDIR)$(PREFIX)/bin/$(OUT)
+
+uninstall:
+	rm -f $(DESTDIR)$(PREFIX)/bin/$(OUT)
+
+.PHONY: all clean dist install uninstall
blob - /dev/null
blob + de3c7c63f8139453fada00a5c97dc1af7abc4ddb (mode 644)
--- /dev/null
+++ README
@@ -0,0 +1,233 @@
+======================================================================
+WARNING: This program is still in progress, use it at your own risk.
+======================================================================
+
+ticl - tiny irc channel linker
+------------------------------
+
+ticl is a very small and simple multi-network irc channel linker.
+
+WARNING: If you use this program on networks that you are not allowed
+	 then your nick or IP may get banned. This is because this
+	 program opens multiple simultaneous connections to the server
+	 and most networks have some limit on each IP.
+
+
+Working
+-------
+
+- A bot named 'linker' joins each given channels and clones each user
+  of its channel on other channels and relays the user's messages
+  received from its channel to the clones on other channels.
+
+- If a user on a channel joins(JOIN), quits(QUIT), leaves(PART), or
+  changes their nick(NICK), the linker attempts to emulate the same
+  action with the respective clones on the other channels.
+
+- Each clone's nick is followed by the original user's channel symbol
+  (enclosed in square brackets).
+
+- If the nick is already taken by another user/clone on that network,
+  an additional suffix '_' is added until the nick is accepted on that
+  network.
+
+- Nick of users or clones that are longer than 16 characters are
+  ignored as some networks only allow nicknames of length 16 or less.
+
+
+Limitations
+-----------
+
+- This program will not work on any channel that requires any kind of
+  registration or verification as clones cannot register.
+
+- Linking the same channel with a different name is undefined and can
+  create an infinite loop of clones.
+
+- Linking more than one channel from a network is undefined.
+
+- No spam protection.
+
+- No support for TLS/SSL.
+
+
+Features
+--------
+
+- Written in OpenBSD's C style(9).
+
+- One clone per user on each channel.
+
+
+Dependencies
+------------
+
+- C compiler (C89)
+
+- C POSIX library
+
+- libbsd (on non-BSD operating systems)
+
+- POSIX make (optional)
+
+
+Installation
+-------------
+
+Edit config.mk for your system.
+
+Then to compile and install, run:
+
+	$ make clean install
+
+To compile without POSIX make on BSD systems, run:
+
+	$ cc -o ticl main.c htable.c
+
+Or on non-BSD systems, run:
+
+	$ cc -o ticl main.c htable.c -lbsd
+
+
+Usages
+------
+
+This program uses FIFO special file (a named pipe) for configuration.
+
+To start the program:
+
+	$ ticl <fifo>
+
+	# Example:
+
+	$ ticl in
+
+	# This will create a 'in' FIFO file if it doesn't already exist.
+
+Or, to start the program with the log printing to a file:
+
+	$ ticl <fifo> > <logfile> 2>&1
+
+	# Example:
+
+	$ ticl in > log 2>&1
+
+	# This will create a 'log' file and print everything to the file.
+
+Or, to start and run the program in background:
+
+	$ ticl <fifo> > <logfile> 2>&1 &
+
+	# Example:
+
+	$ ticl in > log 2>&1 &
+
+To add a channel:
+
+	$ echo 'netadd <name> <symbol> <host> <ip> <channel>' > <fifo>
+
+	# Example, to link #test20 from libera and #test21 from ircnow:
+
+	$ echo 'netadd libera L irc.libera.chat 6667 #test20' > in
+	$ echo 'netadd ircnow N irc.ircnow.org 6667 #test21' > in
+
+To remove a channel from the link:
+
+	$ echo 'netdel <name>' > <fifo>
+
+	# Example, to unlink channel ircnow:
+
+	$ echo 'netdel ircnow' > in
+
+To list all the users:
+
+	$ echo users > <fifo>
+
+To close the program:
+
+	$ echo exit > <fifo>
+
+
+SSL/TLS Support
+---------------
+
+On Openbsd (relayd):
+
+	Edit /etc/relayd.conf:
+
+		table <libera> { irc.libera.chat }
+		table <ircnow> { irc.ircnow.org }
+
+		protocol "irctls" {
+			tcp { nodelay, sack }
+		}
+
+		relay "libera" {
+			listen on 127.0.0.1 port 31220
+			protocol "irctls"
+			forward with tls to <libera> port 6697
+		}
+
+		relay "ircnow" {
+			listen on 127.0.0.1 port 31221
+			protocol "irctls"
+			forward with tls to <ircnow> port 6697
+		}
+
+	To enable and start:
+
+		$ doas rcctl enable relayd
+		$ doas rcctl start relayd
+
+On other platforms (stunnel):
+
+	Edit /etc/stunnel/stunnel.conf:
+
+		pid = /etc/stunnel/pid
+
+		[libera]
+		client = yes
+		accept = 127.0.0.1:31220
+		connect = irc.libera.chat:6697
+		checkHost = irc.libera.chat
+		verifyChain = yes
+		CApath = /etc/ssl/certs
+		OCSPaia = yes
+
+		[ircnow]
+		client = yes
+		accept = 127.0.0.1:31221
+		connect = irc.ircnow.org:6697
+		checkHost = irc.ircnow.org
+		verifyChain = yes
+		CApath = /etc/ssl/certs
+		OCSPaia = yes
+
+	Then enable and start stunnel service.
+
+Now to connect:
+
+	$ echo 'netadd libera L 127.0.0.1 31220 #test20' > in
+	$ echo 'netadd ircnow N 127.0.0.1 31221 #test21' > in
+
+
+Community / Bug Report
+----------------------
+
+Email:	libredev@ircforever.org (expect late response)
+IRC:	#playground on irc.ircnow.org:6697 (TLS)
+
+
+License
+-------
+
+This work is dedicated to the public domain.
+See COPYING file for more information.
+
+
+Note
+----
+
+This work is free software but please don't call it open source as the
+Open Source Initiative (OSI) does not consider public domain software
+as open source. https://opensource.org/node/878
blob - /dev/null
blob + 5abc424404416e3b87406f882ad8cd278a094cfd (mode 644)
--- /dev/null
+++ config.mk
@@ -0,0 +1,14 @@
+# This work is dedicated to the public domain.
+# See COPYING file for more information.
+
+VERSION != date '+%Y-%m-%d'
+
+PREFIX = /usr/local
+MANPREFIX = $(PREFIX)/share/man
+
+CPPFLAGS = -D_DEFAULT_SOURCE -DVERSION=\"$(VERSION)\"
+CFLAGS   = -g -std=c89 -Wall -Wextra -pedantic -Wfatal-errors -Wconversion\
+	   -Wstrict-prototypes -Wold-style-definition $(CPPFLAGS)
+LDFLAGS  = $(LIBS)
+
+CC = cc
blob - /dev/null
blob + d67009a5c7a744b8666a9e79677070d52071546d (mode 644)
--- /dev/null
+++ htable.c
@@ -0,0 +1,227 @@
+/*
+ * This work is dedicated to the public domain.
+ * See COPYING file for more information.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "htable.h"
+
+/*
+ * Destroy the hash table and, if the free_key_val flag is true, free the keys and values.
+ */
+static void
+destroy_and_free(struct htable *ht, int free_key_val)
+{
+	unsigned int i;
+	struct htnode *n, *tmp;
+
+	for (i = 0; i < ht->cap; i++) {
+		for (n = ht->nodes[i]; n != NULL;) {
+			if (free_key_val) {
+				ht->key_free(n->key);
+				ht->val_free(n->val);
+			}
+			tmp = n;
+			n = n->next;
+			free(tmp);
+		}
+	}
+	free(ht->nodes);
+	free(ht);
+}
+
+/*
+ * Insert a new node or, if the replace flag is true, replace the value of an existing node.
+ */
+static int
+insert_or_replace_val(struct htable *ht, void *key, void *val, int replace)
+{
+	unsigned int i;
+	struct htnode *n, *ln;	/* current and last node */
+
+	i = ht->hash(key, ht->cap);
+	ln = NULL;
+	for (n = ht->nodes[i]; n != NULL; n = n->next) {
+		/* if key already exist */
+		if (ht->key_cmp(key, n->key) == 0) {
+			if (replace) {
+				ht->val_free(n->val);
+				n->val = val;
+				return 0;
+			} else {
+				return -1;
+			}
+		}
+		ln = n;
+	}
+
+	if (replace)	/* failed to replace */
+		return -1;
+
+	if ((n = malloc(sizeof(struct htnode))) == NULL) {
+		perror("error: malloc");
+		return -1;
+	}
+	n->key = key;
+	n->val = val;
+	n->next = NULL;
+	/* link to the last node */
+	if (ln == NULL)
+		ht->nodes[i] = n;
+	else
+		ln->next = n;
+	ht->len++;
+	return 0;
+}
+
+/*
+ * Remove a node or, if the replace flag is true, change the key of the node.
+ */
+static int
+remove_or_replace_key(struct htable *ht, void *key, int replace, void *newkey)
+{
+	unsigned int i;
+	struct htnode *n, *ln;	/* current and last node */
+
+	i = ht->hash(key, ht->cap);
+	ln = NULL;
+	for (n = ht->nodes[i]; n != NULL; n = n->next) {
+		if (ht->key_cmp(key, n->key) == 0) {
+			ht->key_free(n->key);
+			if (!replace) {
+				ht->val_free(n->val);
+			} else {
+				if (htinsert(ht, newkey, n->val) == -1)
+					return -1;
+			}
+			/* link to the last node */
+			if (ln == NULL)
+				ht->nodes[i] = n->next;
+			else
+				ln->next = n->next;
+			free(n);
+			return 0;
+		}
+		ln = n;
+	}
+	return -1;
+}
+
+struct htable *
+htcreate(hash_fn *hash, key_cmp_fn *key_cmp, key_free_fn *key_free,
+			val_free_fn *val_free, unsigned int cap)
+{
+	unsigned int i;
+	struct htable *ht;
+
+	if ((ht = malloc(sizeof(struct htable))) == NULL) {
+		perror("error: malloc");
+		return NULL;
+	}
+	if ((ht->nodes = malloc(cap * sizeof(struct htnode *))) == NULL) {
+		perror("error: malloc");
+		return NULL;
+	}
+	for (i = 0; i < cap; i++)
+		ht->nodes[i] = NULL;
+
+	ht->cap		= cap;
+	ht->len		= 0;
+	ht->hash	= hash;
+	ht->key_cmp	= key_cmp;
+	ht->key_free	= key_free;
+	ht->val_free	= val_free;
+	return ht;
+}
+
+void
+htdestroy(struct htable *ht)
+{
+	destroy_and_free(ht, 1);
+}
+
+void *
+htsearch(struct htable *ht, void *key)
+{
+	unsigned int i;
+	struct htnode *n;
+
+	i = ht->hash(key, ht->cap);
+	for (n = ht->nodes[i]; n != NULL; n = n->next) {
+		if (ht->key_cmp(key, n->key) == 0)
+			return n->val;
+	}
+	return NULL;
+}
+
+int
+htinsert(struct htable *ht, void *key, void *val)
+{
+	return insert_or_replace_val(ht, key, val, 0);
+}
+
+int
+htremove(struct htable *ht, void *key)
+{
+	return remove_or_replace_key(ht, key, 0, NULL);
+}
+
+int
+htmodkey(struct htable *ht, void *oldkey, void *newkey)
+{
+	return remove_or_replace_key(ht, oldkey, 1, newkey);
+}
+
+int
+htmodval(struct htable *ht, void *key, void *newval)
+{
+	return insert_or_replace_val(ht, key, newval, 1);
+}
+
+struct htable *
+htresize(struct htable *ht, unsigned int newcap)
+{
+	struct htable	 *newht;
+	struct htiter	  it;
+
+	newht = htcreate(ht->hash, ht->key_cmp, ht->key_free, ht->val_free, newcap);
+	if (newht == NULL)
+		return NULL;
+
+	htiter_init(&it);
+	while(htiterate(ht, &it)) {
+		if (htinsert(newht, it.node->key, it.node->val) == -1) {
+			htdestroy(newht);
+			return NULL;
+		}
+	}
+
+	destroy_and_free(ht, 0);
+	return newht;
+}
+
+void
+htiter_init(struct htiter *it)
+{
+	it->index = 0;
+	it->node = NULL;
+}
+
+int
+htiterate(struct htable *ht, struct htiter *it)
+{
+	if (it->node != NULL)
+		it->node = it->node->next;
+
+	while (it->node == NULL && it->index < ht->cap) {
+		it->node = ht->nodes[it->index];
+		it->index++;
+	}
+
+	if (it->node != NULL)
+		return 1;
+	else
+		return 0;
+}
blob - /dev/null
blob + ab53ccc21392f642725f4039a74eaeaf653c1999 (mode 644)
--- /dev/null
+++ htable.h
@@ -0,0 +1,50 @@
+/*
+ * This work is dedicated to the public domain.
+ * See COPYING file for more information.
+ */
+
+#ifndef HTABLE_H
+#define HTABLE_H
+
+typedef unsigned int	(hash_fn)(void *key, unsigned int cap);
+typedef int		(key_cmp_fn)(const void *key1, const void *key2);
+typedef void		(key_free_fn)(void *key);
+typedef void		(val_free_fn)(void *val);
+
+struct htnode {
+	void		*key;
+	void		*val;
+	struct htnode	*next;
+};
+
+struct htable {
+	struct htnode	**nodes;
+	unsigned int	  cap;
+	unsigned int	  len;
+	hash_fn		 *hash;
+	key_cmp_fn	 *key_cmp;
+	key_free_fn	 *key_free;
+	val_free_fn	 *val_free;
+};
+
+struct htiter {
+	unsigned int	 index;
+	struct htnode	*node;
+};
+
+struct htable	*htcreate(hash_fn *hash,
+			key_cmp_fn *key_cmp,
+			key_free_fn *key_free,
+			val_free_fn *val_free,
+			unsigned int cap);
+void		 htdestroy(struct htable *ht);
+void		*htsearch(struct htable *ht, void *key);
+int		 htinsert(struct htable *ht, void *key, void *val);
+int		 htremove(struct htable *ht, void *key);
+int		 htmodkey(struct htable *ht, void *oldkey, void *newkey);
+int		 htmodval(struct htable *ht, void *key, void *newval);
+struct htable	*htresize(struct htable *ht, unsigned int newcap);
+void		 htiter_init(struct htiter *it);
+int		 htiterate(struct htable *ht, struct htiter *it);
+
+#endif /* HTABLE_H */
blob - /dev/null
blob + fd06b7187e14c868436202bf4389383344160889 (mode 644)
--- /dev/null
+++ main.c
@@ -0,0 +1,1165 @@
+/*
+ * This work is dedicated to the public domain.
+ * See COPYING file for more information.
+ */
+
+#include <errno.h>
+#include <execinfo.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <signal.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifdef __gnu_linux__
+#include <sys/epoll.h>
+#else
+#include <sys/event.h>
+#endif
+
+#define LOG_LEVEL	1
+
+#include "htable.h"
+#include "util.c"
+
+#define CLONE_COOLDOWN	1
+#define CLONE_ADDEND	10
+#define RECONN_TIME	10
+
+#define FD_ADDEND	100
+#define NET_ADDEND	10
+#define USER_ADDEND	100
+
+#define BUFSIZE		1024
+#define NICK_LEN	16
+#define HANDLE_EVENTS	10	/* no. of events to handle at a time */
+
+#define EV_READ		1
+#define EV_WRITE	2
+
+enum {
+	IDLE = 0,
+	RESET,
+	CLONING,
+	EXIT
+};
+
+struct fd_data {
+	char		*user;	/* nick		*/
+	int		netid;	/* net index	*/
+	int	 	suffix;	/* suffix count	*/
+	int	 	ready;	/* joined	*/
+};
+
+struct fd_ref {
+	char		*user;	/* nick		*/
+	int		netid;	/* net index	*/
+	struct fd_ref	*next;	/* next node	*/
+};
+
+struct network {
+	int		 fd;	/* fd		*/
+	char		*name;	/* name		*/
+	char		*symb;	/* symbol	*/
+	char		*host;	/* host		*/
+	char		*port;	/* port		*/
+	char		*chan;	/* channel	*/
+};
+
+static time_t		 ntime = -1;	/* next timeout		*/
+static int		 state = IDLE;	/* program state 	*/
+static int		 fifofd;	/* fifo fd		*/
+static int		 break_evloop;	/* break event loop	*/
+static char		*fifopath;	/* fifo path		*/
+static char		 msg[BUFSIZE];	/* message buffer	*/
+
+#ifdef __gnu_linux__
+static int		 epfd;		/* epoll instance	*/
+#else
+static int		 kqfd;		/* kqueue instance	*/
+#endif
+
+static struct network	 *networks;	/* networks array	*/
+static int		  netlen;	/* array length		*/
+static int		  netcap;	/* array capacity	*/
+static struct fd_data	**fdtodata;	/* fd -> data pointer	*/
+static int		  fdcap;	/* fdtodata capacity	*/
+static struct htable	 *usertofds;	/* user -> array of fds of clones
+					indexed according to networks array */
+
+static struct fd_ref	 *reconn_list_head, /* re-connection queue */
+			 *reconn_list_tail;
+
+/* functions prototype */
+void	 fd_register(int, int);
+void	 fifo_read(void);
+time_t	 event_timeout(void);
+void	 fd_write(int);
+void	 fd_read(int);
+void	 net_add(char *, char *, char *, char *, char *);
+void	 net_del(int, char *, int);
+void	 user_add(char *, int, int);
+void	 user_del(char *, char *);
+void	 reconn_list_add(char *, int);
+void	 reconn_list_del(char *, int);
+int	 fd_new(int, char *);
+void	 fd_del(int, char *, int);
+void	 fd_reconn(int, char *);
+int	*clone_get_fds(char *, int);
+void	 nick_add_symb(char *, int);
+void	 privmsg_update(char *, char *, int, int);
+void	 print_table(void);
+void	 print_htable(void);
+void	 print_users(void);
+void	 print_reconn_list(void);
+void	 print_border(void);
+ssize_t	 writeall(int, char *);
+
+int
+main(int argc, char *argv[])
+{
+#ifdef __gnu_linux__
+	struct epoll_event	epevs[HANDLE_EVENTS];
+#else
+	struct kevent		kevs[HANDLE_EVENTS];
+	struct timespec		tmsp;
+#endif
+	int	i, fd, nev;	/* no. of returned events to handle */
+	time_t	timeout;
+
+	/* set stdout to unbufferd */
+	setvbuf(stdout, NULL, _IONBF, 0);
+
+	/* handle arguments */
+	if (argc != 2)
+		fatal("usage: ticl fifo");
+	else
+		fifopath = argv[1];
+
+	/* init global variables */
+	netcap = NET_ADDEND;
+	fdcap = FD_ADDEND;
+	networks = emalloc((size_t)netcap * sizeof(struct network));
+	fdtodata = emalloc((size_t)fdcap * sizeof(struct fd_data *));
+	usertofds = htcreate(hash_str, (key_cmp_fn *)strcmp, free, free,
+			USER_ADDEND);
+
+#ifdef __gnu_linux__
+	if ((epfd = epoll_create1(0)) == -1)
+		fatal("epoll_create1:");
+#else
+	if ((kqfd = kqueue()) == -1)
+		fatal("kqueue:");
+#endif
+	fifofd = fifo_open(fifopath);
+	fd_register(fifofd, EV_READ);
+
+	/* event loop */
+	while (state != EXIT) {
+		if (ntime == -1) {
+			timeout = -1;
+		} else {
+			timeout = ntime - time(NULL);
+			if (timeout < 0)
+				timeout = 0;
+		}
+#ifdef __gnu_linux__
+		nev = epoll_wait(epfd, epevs, HANDLE_EVENTS, (int)timeout * 1000);
+		if (nev == -1)
+			fatal("epoll_wait:");
+#else
+		tmsp.tv_sec = timeout;
+		tmsp.tv_nsec = 0;
+		nev = kevent(kqfd, NULL, 0, kevs, HANDLE_EVENTS, timeout < 0 ? NULL : &tmsp);
+		if (nev == -1)
+			fatal("kevent:");
+#endif
+		else if (nev == 0) {
+			ntime = event_timeout();
+			continue;
+		}
+
+		for (i = 0; i < nev; i++) {
+#ifdef __gnu_linux__
+			fd = epevs[i].data.fd;
+#else
+			fd = (int)kevs[i].ident;
+#endif
+			if (fd == fifofd) {
+				fifo_read();
+				break;
+#ifdef __gnu_linux__
+			} else if (epevs[i].events & EPOLLOUT) {
+#else
+			} else if (kevs[i].filter == EVFILT_WRITE) {
+#endif
+				fd_write(fd);
+#ifdef __gnu_linux__
+			} else if (epevs[i].events & EPOLLIN) {
+#else
+			} else if (kevs[i].filter == EVFILT_READ) {
+#endif
+				fd_read(fd);
+			} else {
+				fatal("unknown event");
+			}
+
+			if (reconn_list_head != NULL && ntime == -1)
+				ntime = time(0) + RECONN_TIME;
+			if (break_evloop) {
+				break_evloop = FALSE;
+				break;
+			}
+		}
+	}
+
+	/*
+	 * delete and free all the networks with their users
+	 *	- delete in reverse order to prevent swapping.
+	 */
+	snprintf(msg, sizeof(msg), "QUIT :relay shutting down\r\n");
+	for (i = netlen-1; i >= 0; i--)
+		net_del(i, msg, FALSE);
+
+	free(networks);
+	free(fdtodata);
+	htdestroy(usertofds);
+	return 0;
+}
+
+void
+fd_register(int fd, int mode)
+{
+#ifdef __gnu_linux__
+	struct epoll_event ev;
+
+	if (mode == EV_READ)
+		ev.events = EPOLLIN;
+	else if (mode == EV_WRITE)
+		ev.events = EPOLLOUT;
+	else
+		fatal("unknown event mode");
+
+	ev.data.fd = fd;
+	if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
+		fatal("epoll_ctl:");
+#else
+	struct kevent ev;
+	int filter;
+
+	if (mode == EV_READ)
+		filter = EVFILT_READ;
+	else if (mode == EV_WRITE)
+		filter = EVFILT_WRITE;
+	else
+		fatal("unknown event mode");
+
+	EV_SET(&ev, fd, filter, EV_ADD, 0, 0, 0);
+	if (kevent(kqfd, &ev, 1, NULL, 0, NULL) == -1)
+		fatal("kevent:");
+#endif
+}
+
+void
+fifo_read(void)
+{
+	char	 buffer[BUFSIZE];
+	char	*buf;
+	char	*cmd;
+	ssize_t	 n;
+
+	n = readline(fifofd, buffer, sizeof(buffer));
+	if (n == -1) {
+		fatal("read:");
+	} else if (n == 0) {
+		/* reopen fifo again */
+		close(fifofd);
+		fifofd = fifo_open(fifopath);
+		fd_register(fifofd, EV_READ);
+		return;
+	}
+	if (*buffer == '\0')
+		return;
+
+	buf = buffer;
+	cmd = split(&buf, ' ');
+	if (strcmp(cmd, "netadd") == 0) {
+		char *name = split(&buf, ' ');
+		char *symb = split(&buf, ' ');
+		char *host = split(&buf, ' ');
+		char *port = split(&buf, ' ');
+		char *chan = buf;
+		if (!*name || !*symb || !*host || !*port || !*chan)
+			warnf("usage: netadd <name> <symbol> <host> <port> <channel>");
+		else
+			net_add(name, symb, host, port, chan);
+	} else if (strcmp(cmd, "netdel") == 0) {
+		char *name = buf;
+		if (!*name) {
+			warnf("usage: netdel <name>");
+		} else {
+			int i;
+			for (i = 0; i < netlen; i++) {
+				if (strcmp(name, networks[i].name) == 0) {
+					snprintf(msg, sizeof(msg), "QUIT :netdel: %s\r\n", networks[i].name);
+					net_del(i, msg, FALSE);
+					return;
+				}
+			}
+			warnf("%s: network doesn't exist", name);
+		}
+	} else if (strcmp(cmd, "print") == 0) {
+		print_table();
+	} else if (strcmp(cmd, "htable") == 0) {
+		print_htable();
+	} else if (strcmp(cmd, "users") == 0) {
+		print_users();
+	} else if (strcmp(cmd, "reconn") == 0) {
+		print_reconn_list();
+	} else if (strcmp(cmd, "exit") == 0) {
+		state = EXIT;
+	} else {
+		warnf("%s is not a command", cmd);
+	}
+}
+
+time_t
+event_timeout(void)
+{
+	static struct htiter it;
+
+	int i, j, *fds;
+	char *user;
+
+	if (state == IDLE) {
+		struct fd_ref *tmp;
+		debug(1, "Reconnecting");
+		while(reconn_list_head != NULL) {
+			user = reconn_list_head->user;
+			i = reconn_list_head->netid;
+
+			if (user == NULL) {
+				networks[i].fd = fd_new(i, user);
+			} else {
+				if ((fds = htsearch(usertofds, user)) == NULL)
+					fatal("%s: user doesn't exist", user);
+				if (fds[i] == -3 &&
+				    networks[i].fd > 0 &&
+				    fdtodata[networks[i].fd]->ready)
+					fds[i] = fd_new(i, user);
+			}
+			tmp = reconn_list_head;
+			reconn_list_head = reconn_list_head->next;
+			free(tmp);
+		}
+		reconn_list_tail = reconn_list_head;
+		return -1;
+	} else if (state == RESET) {
+		state = CLONING;
+		htiter_init(&it);
+	}
+	debug(1, ".");
+
+	j = 0;
+	while (htiterate(usertofds, &it)) {
+		user = (char *)it.node->key;
+		fds = (int *)it.node->val;
+		for (i = 0; i < netlen; i++) {
+			if (fds[i] == 0 && networks[i].fd > 0 &&
+					fdtodata[networks[i].fd]->ready)
+				fds[i] = fd_new(i, user);
+		}
+		j++;
+		if (j >= CLONE_ADDEND)
+			return time(NULL) + CLONE_COOLDOWN;
+	}
+	state = IDLE;
+	return -1;
+}
+
+void
+fd_write(int fd)
+{
+#ifdef __gnu_linux__
+	struct epoll_event ev;
+	ev.events = EPOLLIN;
+	ev.data.fd = fd;
+	if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1)
+		fatal("epoll_ctl:");
+#else
+	struct kevent ev;
+	EV_SET(&ev, fd, EVFILT_WRITE, EV_DISABLE, 0, 0, 0);
+	if (kevent(kqfd, &ev, 1, NULL, 0, NULL) == -1)
+		fatal("kevent:");
+	fd_register(fd, EV_READ);
+#endif
+	if (fdtodata[fd]->user == NULL) {	/* watcher */
+		snprintf(msg, sizeof(msg), "NICK watcher\r\nUSER watcher 0 * :watcher\r\n");
+		writeall(fd, msg);
+	} else {			/* user */
+		snprintf(msg, sizeof(msg), "NICK %s\r\nUSER user 0 * :user\r\n", fdtodata[fd]->user);
+		writeall(fd, msg);
+	}
+}
+
+void
+fd_read(int fd)
+{
+	char	 buffer	[BUFSIZE];
+	char	 backup	[BUFSIZE];
+	char	 wnick	[NICK_LEN];	/* watcher nick */
+	char	*buf;
+	char	*cmd;
+	char	*nick;
+	int	 i, netid;
+	ssize_t	 n;
+
+	netid = fdtodata[fd]->netid;
+
+	n = readline(fd, buffer, sizeof(buffer));
+	if (n == -1) {
+		warnf("%d: read:", fd);
+		snprintf(msg, sizeof(msg), "QUIT :read failed\r\n");
+		fd_reconn(fd, msg);
+		return;
+	} else if (n == 0) {
+		warnf("%d: read: connection closed", fd);
+		snprintf(msg, sizeof(msg), "QUIT :connection closed\r\n");
+		fd_reconn(fd, msg);
+		return;
+	}
+	if (*buffer == '\0')
+		return;
+
+	/* clone the buffer */
+	strlcpy(backup, buffer, sizeof(backup));
+	buf = buffer;
+
+	/* set watcher nick */
+	strlcpy(wnick, "watcher", sizeof(wnick));
+	for (i = 0; i < fdtodata[fd]->suffix; i++)
+		strcat(wnick, "_");
+
+	/* first column */
+	cmd = split(&buf, ' ');
+	if (strcmp(cmd, "NOTICE") == 0) {	/* ignore */
+		return;
+	} else if (strcmp(cmd, "ERROR") == 0) {
+		warnf("%d: %s", fd, backup);
+		snprintf(msg, sizeof(msg), "QUIT :ERROR\r\n");
+		fd_reconn(fd, msg);
+		return;
+	} else if (strcmp(cmd, "PING") == 0) {
+		snprintf(msg, sizeof(msg), "PONG %s\r\n", buf);
+		writeall(fd, msg);
+		return;
+	}
+	/* strip nick from first column */
+	nick = split(&cmd, '!');
+	if (nick[0] == ':')
+		nick++;
+
+	/* second column */
+	cmd = split(&buf, ' ');
+
+	/* ignore all the info messages */
+	if ((strcmp(cmd, "002") == 0) ||
+	    (strcmp(cmd, "003") == 0) ||
+	    (strcmp(cmd, "004") == 0) ||
+	    (strcmp(cmd, "005") == 0) ||
+	    (strcmp(cmd, "251") == 0) ||
+	    (strcmp(cmd, "252") == 0) ||
+	    (strcmp(cmd, "253") == 0) || /* unknown connection(s) */
+	    (strcmp(cmd, "254") == 0) ||
+	    (strcmp(cmd, "255") == 0) ||
+	    (strcmp(cmd, "265") == 0) ||
+	    (strcmp(cmd, "266") == 0) ||
+	    (strcmp(cmd, "250") == 0) ||
+	    (strcmp(cmd, "332") == 0) ||
+	    (strcmp(cmd, "333") == 0) ||
+	    (strcmp(cmd, "375") == 0) ||
+	    (strcmp(cmd, "372") == 0) ||
+	    (strcmp(cmd, "376") == 0) ||
+	    (strcmp(cmd, "396") == 0) ||
+	    (strcmp(cmd, "366") == 0) ||
+	    (strcmp(cmd, "MODE") == 0) ||
+	    (strcmp(cmd, "TOPIC") == 0) ||
+	    (strcmp(cmd, "NOTICE") == 0)) {
+		return;
+	} else if (strcmp(cmd, "432") == 0) {	/* Erroneous Nickname */
+		fatal("%d: %s", fd, backup);
+	} else if (strcmp(cmd, "433") == 0) {	/* Nickname already in use */
+		split(&buf, ' ');
+		nick = split(&buf, ' ');
+		strcat(nick, "_");
+		fdtodata[fd]->suffix++;
+		if (strlen(nick) > NICK_LEN) {
+			warnf("%s: nickname too long", nick);
+			snprintf(msg, sizeof(msg), "QUIT :%s: nickname too long\r\n", nick);
+			if (fdtodata[fd]->user == NULL)
+				net_del(netid, msg, FALSE);
+			else
+				fd_del(fd, msg, FALSE);
+			return;
+		} else {
+			snprintf(msg, sizeof(msg), "NICK %s\r\n", nick);
+			writeall(fd, msg);
+		}
+		return;
+	} else if (strcmp(cmd, "001") == 0) {
+		snprintf(msg, sizeof(msg), "JOIN %s\r\n", networks[netid].chan);
+		writeall(fd, msg);
+		return;
+	} else if (strcmp(cmd, "PRIVMSG") == 0) {
+		char privmsg[BUFSIZE] = "";
+		int *fds;
+
+		if (fdtodata[fd]->user == NULL) {	/* if watcher */
+			nick_add_symb(nick, netid);
+			if ((fds = htsearch(usertofds, nick)) == NULL)
+				return;
+
+			split(&buf, ':');	/* set buf to msg */
+			for (i = 0; i < netlen; i++) {
+				printf("%s\n", buf);
+				if (fds[i] > 0) {
+					privmsg_update(privmsg, buf, netid, fds[i]);
+					snprintf(msg, sizeof(msg), "PRIVMSG %s :%s\r\n", networks[i].chan, privmsg);
+					writeall(fds[i], msg);
+				}
+			}
+		} else {
+			char *netsymb;
+			char *user = split(&buf, ' ');
+
+			/* ignore messages from channel (it is handled by watcher) */
+			if (user[0] == '#' || user[0] == '&')
+				return;
+
+			nick_add_symb(nick, netid);
+			if ((fds = htsearch(usertofds, nick)) == NULL)
+				return;
+
+			/* split user nick and network symbol */
+			*strrchr(user, ']') = '\0';
+			netsymb = strrchr(user, '[');
+			*netsymb++ = '\0';
+
+			/* get the network index */
+			for (i = 0; i < netlen; i++) {
+				if (strcmp(netsymb, networks[i].symb) == 0)
+					break;
+			}
+
+			split(&buf, ':');	/* set buf to msg */
+			privmsg_update(privmsg, buf, netid, fds[i]);
+			snprintf(msg, sizeof(msg), "PRIVMSG %s :%s\r\n", user, privmsg);
+			writeall(fds[i], msg);
+		}
+		return;
+	}
+	else if (strcmp(cmd, "353") == 0) {
+		fdtodata[fd]->ready = TRUE;
+		/* FALLBACK */
+	}
+
+	/* from now, the messages are handled by watcher */
+	if (fdtodata[fd]->user != NULL)	/* if clone */
+		return;
+
+	if (strcmp(cmd, "353") == 0) {
+		char *nick;
+		split(&buf, ':');
+		state = RESET;
+		ntime = 0;
+
+		/* then add all new users */
+		while (*(nick = split(&buf, ' ')) != '\0') {
+			if (*nick == '@' ||
+			    *nick == '&' ||
+			    *nick == '~' ||
+			    *nick == '%' ||
+			    *nick == '+' ||
+			    *nick == '\\')
+				nick++;
+			if (strcmp(nick, wnick) != 0)
+				user_add(nick, netid, FALSE);
+		}
+		return;
+	} else if (strcmp(cmd, "JOIN") == 0) {
+		/* if real user */
+		if ((strcmp(nick, wnick) != 0) &&
+		    (clone_get_fds(nick, netid) == NULL)) {
+			if (state != IDLE)
+				warnf("%s: ignored (network cloning)", nick);
+			else
+				user_add(nick, netid, TRUE);
+		}
+		return;
+	} else if ((strcmp(cmd, "QUIT") == 0) || (strcmp(cmd, "PART") == 0)) {
+		split(&buf, ':'); /* ignore ':' and assign QUIT/PART msg to buf */
+		snprintf(msg, sizeof(msg), "QUIT :%s\r\n", buf);
+		nick_add_symb(nick, netid);
+		if (htsearch(usertofds, nick) != NULL)
+			user_del(nick, msg);
+		return;
+	} else if (strcmp(cmd, "NICK") == 0) {
+		int *fds, i;
+		char *newnick;
+
+		nick_add_symb(nick, netid);
+		if ((fds = htsearch(usertofds, nick)) == NULL)
+			return;
+
+		/* set buf to new nick */
+		split(&buf, ':');
+		/* allocate a new nick with net symbol and replace the old one */
+		newnick = emalloc((strlen(buf) + strlen(networks[netid].symb) + 2 + 1) * sizeof(char));
+		sprintf(newnick, "%s[%s]", buf, networks[netid].symb);
+		snprintf(msg, sizeof(msg), "NICK %s\r\n", newnick);
+		for (i = 0; i < netlen; i++) {
+			if (fds[i] > 0) {
+				fdtodata[fds[i]]->user = newnick;
+				fdtodata[fds[i]]->suffix = 0;
+				writeall(fds[i], msg);
+			} else if (fds[i] == -3) {
+				reconn_list_del(nick, i);
+				reconn_list_add(newnick, i);
+			}
+		}
+		htmodkey(usertofds, nick, newnick);
+		return;
+	} else if (strcmp(cmd, "KICK") == 0) {
+		/* :<user_who_kicked>!~username@host KICK <channel> <user_who_is_being_kicked> :<kick_msg> */
+		int *fds;
+		char *user;
+
+		split(&buf, ' ');		/* channel name				*/
+		user = split(&buf, ' ');	/* user who is being kicked		*/
+		split(&buf, ':');		/* ignore ':' and store reason to buf	*/
+
+		/* if watcher is being kicked, delete the network */
+		if (strcmp(user, wnick) == 0) {
+			snprintf(msg, sizeof(msg), "QUIT :netdel: %s (%s is kicked by %s)\r\n",
+					networks[netid].name, wnick, nick);
+			fd_reconn(fd, msg);
+			return;
+		}
+
+		/* if message is from a real user, delete the user */
+		if ((fds = clone_get_fds(user, netid)) == NULL) {
+			snprintf(msg, sizeof(msg), "QUIT :userdel: %s (kicked by %s)\r\n", user, nick);
+			nick_add_symb(user, netid);
+			user_del(user, msg);
+			return;
+		}
+
+		/* delete the kicked clone */
+		snprintf(msg, sizeof(msg), "QUIT :kicked by %s\r\n", nick);
+		fd_reconn(fds[netid], msg);
+
+		/* get the original user netid */
+		for (i = 0; i < netlen; i++) {
+			if (fds[i] == -1)
+				break;
+		}
+		/* send notice in the channel through watcher */
+		if (networks[i].fd > 0) {
+			snprintf(msg, sizeof(msg), "PRIVMSG %s :%s is kicked by %s [%s]\r\n",
+					networks[i].chan, user, nick, buf);
+			writeall(networks[i].fd, msg);
+		}
+		return;
+	}
+	warnf("%d: %s", fd, backup);
+	return;
+}
+
+void
+net_add(char *name, char *symb, char *host, char *port, char *chan)
+{
+	struct network *n;
+	int i;
+
+	for (i = 0; i < netlen; i++) {
+		if (strcmp(networks[i].name, name) == 0) {
+			warnf("%s: network name already exists", name);
+			return;
+		}
+		if (strcmp(networks[i].symb, symb) == 0) {
+			warnf("%s: network symbol already exists", symb);
+			return;
+		}
+		if ((strcmp(networks[i].host, host) == 0) &&
+		    (strcmp(networks[i].port, port) == 0) &&
+		    (strcmp(networks[i].chan, chan) == 0)) {
+			warnf("%s:%s%s: network configuration already exists",
+			    host, port, chan);
+			return;
+		}
+	}
+
+	/* if full, resize the network and user fds */
+	if (netlen == netcap) {
+		struct htiter it;
+		htiter_init(&it);
+
+		networks = erealloc(networks, (size_t)(netcap + NET_ADDEND) *
+				sizeof(struct network));
+		while (htiterate(usertofds, &it)) {
+			it.node->val = erealloc(it.node->val,
+					(size_t)(netcap + NET_ADDEND) *
+					sizeof(int));
+			/* zero out the extended array */
+			for (i = netcap; i < netcap + NET_ADDEND; i++)
+				((int *)it.node->val)[i] = 0;
+		}
+		netcap += NET_ADDEND;
+	}
+
+	/* add a network */
+	n = &networks[netlen];
+	n->name = strdup(name);
+	n->symb = strdup(symb);
+	n->host = strdup(host);
+	n->port = strdup(port);
+	n->chan = strdup(chan);
+	n->fd	= fd_new(netlen, NULL);
+	netlen++;
+}
+
+void
+net_del(int netid, char *msg, int reconnect)
+{
+	int *fds;
+	char *user;
+	struct network *n;
+	struct htiter lit, it;	/* last, current iterator */
+
+	n = &networks[netid];
+	htiter_init(&it);
+	htiter_init(&lit);
+	while (htiterate(usertofds, &it)) {
+		user = (char *)it.node->key;
+		fds = (int *)it.node->val;
+		if (fds[netid] == -1) {
+			user_del(it.node->key, msg);
+			it = lit; /* this node is deleted */
+		} else {
+			if (fds[netid] > 0)
+				fd_del(fds[netid], msg, FALSE);
+			else if (fds[netid] == -3)
+				reconn_list_del(user, netid);
+
+			if (!reconnect) {
+				if(netid != netlen-1) {
+					fds[netid] = fds[netlen-1];
+					if (fds[netid] > 0) {
+						fdtodata[fds[netid]]->netid = netid;
+					} else if (fds[netid] == -3) {
+						reconn_list_del(user, netlen-1);
+						reconn_list_add(user, netid);
+					}
+				}
+				fds[netlen-1] = 0;
+			} else {
+				fds[netid] = 0;
+			}
+		}
+		lit = it;
+	}
+
+	if (n->fd > 0)
+		fd_del(n->fd, msg, reconnect);
+	else if (n->fd == -3)
+		reconn_list_del(NULL, netid);
+
+	if (!reconnect) {
+		free(n->name);
+		free(n->symb);
+		free(n->host);
+		free(n->port);
+		free(n->chan);
+		/* swap */
+		if(netid != netlen-1) {
+			networks[netid] = networks[netlen-1];
+			if (networks[netid].fd > 0) {
+				fdtodata[n->fd]->netid = netid;
+			} else if (networks[netid].fd == -3) {
+				reconn_list_del(NULL, netlen-1);
+				reconn_list_add(NULL, netid);
+			}
+		}
+		netlen--;
+	}
+}
+
+void
+user_add(char *unick, int netid, int clone)
+{
+	size_t	 len;
+	char	*nick;
+	int	*fds;
+
+	len = strlen(unick) + strlen(networks[netid].symb) + 2 + 1;
+	if (len-1 > NICK_LEN) {
+		warnf("%s[%s]: nickname too long", unick, networks[netid].symb);
+		return;
+	}
+
+	/* resize hash table if storage is low */
+	if ((usertofds->cap - usertofds->len) < USER_ADDEND)
+		usertofds = htresize(usertofds, usertofds->cap + USER_ADDEND);
+
+	/* allocate a new user */
+	nick = emalloc(len * sizeof(char));
+	fds = ecalloc((size_t)netcap, sizeof(int));
+	sprintf(nick, "%s[%s]", unick, networks[netid].symb);
+	fds[netid] = -1;
+
+	if (clone) {
+		int i;
+		for (i = 0; i < netlen; i++) {
+			if (fds[i] == 0 && networks[i].fd > 0 &&
+					fdtodata[networks[i].fd]->ready)
+				fds[i] = fd_new(i, nick);
+		}
+	}
+
+	if (htinsert(usertofds, nick, fds) == -1)
+		fatal("%s: user already exists", nick);
+}
+
+void
+user_del(char *user, char *msg)
+{
+	int i, *fds;
+
+	if ((fds = htsearch(usertofds, user)) == NULL)
+		fatal("%s: user doesn't exist", user);
+	for (i = 0; i < netlen; i++) {
+		if (fds[i] > 0) {
+			fd_del(fds[i], msg, FALSE);
+		} else if (fds[i] == -3) {
+			reconn_list_del(user, i);
+		}
+	}
+	htremove(usertofds, user);
+}
+
+void
+reconn_list_add(char *user, int netid)
+{
+	struct fd_ref *node;
+
+	node = emalloc(sizeof(struct fd_ref));
+	node->user = user;
+	node->netid = netid;
+	node->next = NULL;
+	if (reconn_list_tail == NULL) {
+		reconn_list_head = reconn_list_tail = node;
+	} else {
+		reconn_list_tail->next = node;
+		reconn_list_tail = reconn_list_tail->next;
+	}
+}
+
+void
+reconn_list_del(char *user, int netid)
+{
+	struct fd_ref *n, *pn;	/* current and previous node */
+
+	n = reconn_list_head;
+	pn = NULL;
+	while (n != NULL) {
+		if (n->netid == netid &&
+		    ((user == NULL && n->user == NULL) ||
+		     (user != NULL && n->user != NULL &&
+		      (strcmp(n->user, user) == 0)))) {
+			if (n == reconn_list_head && n == reconn_list_tail)
+				reconn_list_head = reconn_list_tail = NULL;
+			else if (n == reconn_list_head)
+				reconn_list_head = n->next;
+			else if (n == reconn_list_tail)
+				reconn_list_tail = pn;
+			else
+				pn->next = n->next;
+			free(n);
+			return;
+		}
+		pn = n;
+		n = n->next;
+	}
+	fatal("%d:%s: failed to find in re-connection list", netid, user);
+}
+
+int
+fd_new(int netid, char *user)
+{
+	int fd;
+	struct network *n;
+	struct fd_data *data;
+
+	n = &networks[netid];
+	fd = dial(n->host, n->port);
+	if (fd == -1) {
+		warnf("%s:%s: failed to connect", n->host, n->port);
+		return -2;
+	}
+	fd_register(fd, EV_WRITE);
+
+	if (user == NULL)
+		debug(1, "%d: netadd: %s[%s]", fd, n->name, n->symb);
+	else
+		debug(1, "%d: add[%s]: %s", fd, n->symb, user);
+
+	if (fd+1 > fdcap) {
+		fdcap *= 2;
+		fdtodata = erealloc(fdtodata, (size_t)fdcap * sizeof(struct fd_data *));
+	}
+
+	data = emalloc(1 * sizeof(struct fd_data));
+	data->netid = netid;
+	data->user = user;
+	data->suffix = 0;
+	data->ready = FALSE;
+
+	fdtodata[fd] = data;
+
+	return fd;
+}
+
+void
+fd_del(int fd, char *msg, int reconnection)
+{
+	char	*user;
+	int	 netid;
+	int	*fds;
+
+	user = fdtodata[fd]->user;
+	netid = fdtodata[fd]->netid;
+
+	if (user == NULL) {
+		debug(1, "%d: netdel: %s[%s]", fd, networks[netid].name, networks[netid].symb);
+		networks[netid].fd = reconnection ? -3 : -2;
+	} else {
+		debug(1, "%d: del[%s]: %s", fd, networks[netid].symb, user);
+		if ((fds = htsearch(usertofds, user)) == NULL)
+			fatal("%s: user doesn't exist", user);
+		fds[netid] = reconnection ? -3 : -2;
+	}
+
+	if (fdtodata[fd]->ready)
+		writeall(fd, msg);
+	close(fd);
+
+	if (reconnection)
+		reconn_list_add(user, netid);
+	free(fdtodata[fd]);
+	fdtodata[fd] = NULL;
+	break_evloop = TRUE;
+}
+
+void
+fd_reconn(int fd, char *msg)
+{
+	if (fdtodata[fd]->user == NULL)
+		net_del(fdtodata[fd]->netid, msg, TRUE);
+	else
+		fd_del(fd, msg, TRUE);
+}
+
+int *
+clone_get_fds(char *nick, int netid)
+{
+	unsigned int s;
+	int *fds = NULL;
+
+	/* count suffix */
+	for (s = 0; nick[strlen(nick)-s-1] == '_'; s++);
+	/* remove suffix */
+	if (s > 0)
+		nick[strlen(nick)-s] = '\0';
+
+	fds = htsearch(usertofds, nick);
+	/* if match but suffix doesn't match */
+	if ((fds != NULL) && (fdtodata[fds[netid]]->suffix != (int)s))
+		fds = NULL;
+
+	/* add suffix back */
+	if (s > 0)
+		nick[strlen(nick)] = '_';
+
+	return fds;
+}
+
+void
+nick_add_symb(char *nick, int netid)
+{
+	strcat(nick, "[");
+	strcat(nick, networks[netid].symb);
+	strcat(nick, "]");
+}
+
+/*
+ * trim all the nicknames to original nick
+ * src will be destructed
+ */
+void
+privmsg_update(char *dst, char *src, int netid, int fd)
+{
+	char d;		/* delimiter */
+	char *n;
+	int i, *fds;
+
+	while (src != NULL) {
+		n = strpbrk(src, " :;,<>@&~%+\\");
+		if (n == NULL) {
+			d = '\0';
+		} else {
+			d = *n;
+			*n = '\0';
+			n++;
+		}
+
+		/* check if the word is nick */
+		if ((fds = clone_get_fds(src, netid)) != NULL) {
+			int netid2 = fdtodata[fd]->netid;
+			*strrchr(src, '[') = '\0';
+			src[strlen(src)] = '[';
+			if (fds[netid2] > 0) {
+				nick_add_symb(dst, netid2);
+				for (i = 0; i < fdtodata[fds[netid2]]->suffix; i++)
+					strcat(dst, "_");
+			}
+		} else {
+			strcat(dst, src);
+		}
+
+		strncat(dst, &d, 1);
+		src = n;
+	}
+}
+
+void
+print_table(void)
+{
+	int	i, *fds, diff, tabs;
+	char	*nick;
+	struct htiter it;
+
+	if (netlen == 0)
+		return;
+
+	print_border();
+	/* print networks */
+	printf("Networks\t\t");
+	for (i = 0; i < netlen; i++) {
+		printf("%s->%d", networks[i].symb, networks[i].fd);
+		/* print suffix */
+		if (networks[i].fd > 0 && fdtodata[networks[i].fd]->suffix > 0)
+			printf("(%d)\t", fdtodata[networks[i].fd]->suffix);
+		else
+			printf("\t\t");
+	}
+	printf("\n");
+
+	htiter_init(&it);
+	while (htiterate(usertofds, &it)) {
+		fds  = (int *)it.node->val;
+		nick = (char *)it.node->key;
+		/* print tabbed user nick */
+		printf("%s", nick);
+		diff = 24 - (int)strlen(nick);
+		tabs = ((diff / 8) + (diff % 8 > 0));
+		printf("%.*s", tabs, "\t\t\t");
+		/* print tabbed fds */
+		for (i = 0; i < netlen; i++) {
+			printf("%d", fds[i]);
+			/* print suffix */
+			if ((fds[i] > 0) && (fdtodata[fds[i]]->suffix > 0))
+				printf("(%d)", fdtodata[fds[i]]->suffix);
+			printf("\t\t");
+		}
+		printf("\n");
+	}
+	print_border();
+}
+
+void
+print_htable(void)
+{
+	struct htiter it;
+	int index = -1;
+
+	print_border();
+	htiter_init(&it);
+	while (htiterate(usertofds, &it)) {
+		if (index != (int)it.index) {
+			/* ignore first new line */
+			if (index != -1)
+				printf("\n");
+			printf("%d", it.index-1);
+			index = (int)it.index;
+		}
+		printf(" -> %s", (char *)it.node->key);
+	}
+	printf("\n");
+	print_border();
+}
+
+void
+print_users(void)
+{
+	struct htiter it;
+	int i = 0;
+
+	print_border();
+	htiter_init(&it);
+	while (htiterate(usertofds, &it))
+		printf("%d: %s\n", i++, (char *)it.node->key);
+	print_border();
+}
+
+void
+print_reconn_list(void)
+{
+	struct fd_ref *n;
+
+	print_border();
+	n = reconn_list_head;
+	while(n != NULL) {
+		printf("%d: %s\n", n->netid, n->user);
+		n = n->next;
+	}
+	print_border();
+}
+
+void
+print_border(void)
+{
+	int i;
+	for (i = 0; i < 64; i++)
+		printf("-");
+	printf("\n");
+}
+
+ssize_t
+writeall(int fd, char *buf)
+{
+	ssize_t left, sent, n;
+	left = (ssize_t)strlen(buf);
+	sent = 0;
+	while (sent < left) {
+		if ((n = write(fd, buf+sent, (size_t)left)) == -1) {
+			warnf("%d: write:", fd);
+			snprintf(msg, sizeof(msg), "QUIT :write failed\r\n");
+			fd_reconn(fd, msg);
+			return -1;
+		}
+		sent += n;
+		left -= n;
+	}
+	return sent;
+}
blob - /dev/null
blob + ddda07daf6a884fc4d3a1bc35297ac069a67ac71 (mode 644)
--- /dev/null
+++ util.c
@@ -0,0 +1,210 @@
+/*
+ * This work is dedicated to the public domain.
+ * See COPYING file for more information.
+ */
+
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define FALSE	0
+#define TRUE	1
+
+#ifdef __gnu_linux__
+char *strlcpy(char *dst, const char *src, size_t n)
+{
+	return strncpy(dst, src, n);
+}
+#endif
+
+void
+print_log(int level, FILE *stream, const char *fmt, va_list arg)
+{
+	size_t	len;
+
+	if (level > LOG_LEVEL)
+		return;
+
+	vfprintf(stream, fmt, arg);
+	len = strlen(fmt);
+	if (len && fmt[len-1] == ':')
+		fprintf(stream, " %s\n", strerror(errno));
+	else
+		fprintf(stream, "\n");
+}
+
+void
+debug(int level, const char *fmt, ...)
+{
+	va_list arg;
+	va_start(arg, fmt);
+	print_log(level, stdout, fmt, arg);
+	va_end(arg);
+}
+
+void
+warnf(const char *fmt, ...)
+{
+	va_list arg;
+	fprintf(stderr, "warn: ");
+	va_start(arg, fmt);
+	print_log(0, stderr, fmt, arg);
+	va_end(arg);
+}
+
+void
+fatal(const char *fmt, ...)
+{
+	va_list arg;
+	fprintf(stderr, "error: ");
+	va_start(arg, fmt);
+	print_log(0, stderr, fmt, arg);
+	va_end(arg);
+	exit(1);
+}
+
+void *
+emalloc(size_t size)
+{
+	void *p;
+	if ((p = malloc(size)) == NULL)
+		fatal("malloc:");
+	return p;
+}
+
+void *
+ecalloc(size_t nmemb, size_t size)
+{
+	void *p;
+	if ((p = calloc(nmemb, size)) == NULL)
+		fatal("calloc:");
+	return p;
+}
+
+void *
+erealloc(void *ptr, size_t size)
+{
+	void *p;
+	if ((p = realloc(ptr, size)) == NULL)
+		fatal("realloc:");
+	return p;
+}
+
+char*
+split(char **str, char ch)
+{
+	char *token = *str;
+
+	if (**str == '\0') return *str;
+
+	while (**str != ch && **str != '\0')
+		(*str)++;
+	if (**str == '\0')
+		return token;
+	**str = '\0';
+	(*str)++;
+	while(**str == ch && **str != '\0')
+		(*str)++;
+	return token;
+}
+
+int
+dial(char *host, char *port)
+{
+	static struct addrinfo hints, *res = NULL, *res0;
+	int fd = -1, r;
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+
+	if ((r = getaddrinfo(host, port, &hints, &res0)) != 0) {
+		warnf("getaddrinfo: %s", gai_strerror(r));
+		return -1;
+	}
+	for (res = res0; res != NULL; res = res->ai_next) {
+		fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+		if (fd == -1) {
+			warnf("socket:");
+			continue;
+		}
+		if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) {
+			warnf("fnctl:");
+			continue;
+		}
+		if ((connect(fd, res->ai_addr, res->ai_addrlen) == -1) &&
+		    (errno == EINPROGRESS))
+			break;
+		warnf("connect:");
+		close(fd);
+		fd = -1;
+	}
+	if (fd == -1)
+		warnf("failed to connect to %s", host);
+
+	freeaddrinfo(res0);
+	return fd;
+}
+
+int
+fifo_open(char *path)
+{
+	struct stat st;
+	int fd;
+
+	/* make a fifo file if it doesn't exists */
+	if (lstat(path, &st) != -1) {
+		if (!(st.st_mode & S_IFIFO))
+			fatal("%s: not a fifo file", path);
+	} else if (mkfifo(path, S_IRWXU) == -1) {
+		fatal("mkfifo: %s:", path);
+	}
+
+	fd = open(path, O_RDONLY | O_NONBLOCK, 0);
+	if (fd == -1)
+		fatal("open: %s:", path);
+
+	return fd;
+}
+
+ssize_t
+readline(int fd, char *buffer, size_t size)
+{
+	size_t i = 0;
+	char c;
+	ssize_t l;
+
+	while (i < size) {
+		l = read(fd, &c, sizeof(char));
+		if (l == -1 && errno == EAGAIN)
+			continue;
+		else if (l == -1 || l == 0)
+			return l;
+
+		if (c == '\r' || c == '\n')
+			break;
+		buffer[i++] = c;
+	}
+	buffer[i++] = '\0';
+	return (ssize_t)i;
+}
+
+/* very simple string hash function */
+unsigned int
+hash_str(void *key, unsigned int cap)
+{
+	unsigned int i, sum = 0;
+
+	for (i = 0; i < (unsigned int)strlen(key); i++) {
+		sum += ((unsigned char *)key)[i] * (i + 1);
+	}
+	return (sum % cap);
+}