commit f60772b95eed41b965f4de47c088eeb81e7bc607 from: jrmu date: Fri Jul 18 20:13:30 2025 UTC Import sources ticl 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 $/; }; + + # 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: +# +# +# +# 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 &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 &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 ($_ = )) { + 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 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&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 &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 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 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 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 + + # 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 > 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 > 2>&1 & + + # Example: + + $ ticl in > log 2>&1 & + +To add a channel: + + $ echo 'netadd ' > + + # 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 ' > + + # Example, to unlink channel ircnow: + + $ echo 'netdel ircnow' > in + +To list all the users: + + $ echo users > + +To close the program: + + $ echo exit > + + +SSL/TLS Support +--------------- + +On Openbsd (relayd): + + Edit /etc/relayd.conf: + + table { irc.libera.chat } + table { irc.ircnow.org } + + protocol "irctls" { + tcp { nodelay, sack } + } + + relay "libera" { + listen on 127.0.0.1 port 31220 + protocol "irctls" + forward with tls to port 6697 + } + + relay "ircnow" { + listen on 127.0.0.1 port 31221 + protocol "irctls" + forward with tls to 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 +#include + +#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 +#include +#include +#include +#include +#include +#include +#include + +#ifdef __gnu_linux__ +#include +#else +#include +#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 "); + else + net_add(name, symb, host, port, chan); + } else if (strcmp(cmd, "netdel") == 0) { + char *name = buf; + if (!*name) { + warnf("usage: netdel "); + } 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) { + /* :!~username@host KICK : */ + 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#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); +}