[xiph-commits] r12591 - in trunk/ezstream: . conf doc src
moritz at svn.xiph.org
moritz at svn.xiph.org
Wed Feb 28 13:26:23 PST 2007
Author: moritz
Date: 2007-02-28 13:26:16 -0800 (Wed, 28 Feb 2007)
New Revision: 12591
Modified:
trunk/ezstream/NEWS
trunk/ezstream/conf/ezstream_reencoding_example_mp3.xml
trunk/ezstream/conf/ezstream_reencoding_example_vorbis.xml
trunk/ezstream/doc/ezstream.1.in
trunk/ezstream/src/configfile.c
trunk/ezstream/src/configfile.h
trunk/ezstream/src/ezstream.c
trunk/ezstream/src/playlist.c
trunk/ezstream/src/playlist.h
Log:
Add new playlist scripting feature (works similar to Ices 2.x.)
Modified: trunk/ezstream/NEWS
===================================================================
--- trunk/ezstream/NEWS 2007-02-28 15:35:07 UTC (rev 12590)
+++ trunk/ezstream/NEWS 2007-02-28 21:26:16 UTC (rev 12591)
@@ -6,6 +6,9 @@
* New features:
- Playlist shuffling support, enabled via the new <shuffle> configuration
option.
+ - Playlist scripting support: Indicate that the executable in <filename>
+ should be run each time to get a new media filename to stream, by setting
+ the new <playlist_program> configuration option to 1.
- Add feature to skip the currently streaming track, done by sending the
SIGUSR1 signal to the ezstream process.
- New command line option `-q': Suppress standard error output from external
@@ -15,6 +18,8 @@
on the command line.
- Thorough configuration file checks with helpful error messages.
- The @M@ metadata placeholder is now supported in <decode>.
+ - Playlists may now have the '.txt' filename extension in addition to
+ '.m3u'.
* Fixes:
- At least one stack and one heap overflow have been fixed.
Modified: trunk/ezstream/conf/ezstream_reencoding_example_mp3.xml
===================================================================
--- trunk/ezstream/conf/ezstream_reencoding_example_mp3.xml 2007-02-28 15:35:07 UTC (rev 12590)
+++ trunk/ezstream/conf/ezstream_reencoding_example_mp3.xml 2007-02-28 21:26:16 UTC (rev 12591)
@@ -1,5 +1,5 @@
<!--
- EXAMPLE: MP3 playlist stream WITH reencoding and sequential playback
+ EXAMPLE: MP3 stream using an external playlist program, WITH reencoding
This example streams a playlist that may contain MP3, Ogg Vorbis and FLAC
files. Ezstream will use external decoders to read the media files, and
@@ -14,12 +14,12 @@
output format of the stream.
-->
<format>MP3</format>
- <filename>playlist.m3u</filename>
+ <filename>playlist.pl</filename>
<!--
- Explicitly disable playlist shuffling. Sequential playback is also the
- default.
+ Indicate that <filename> contains an executable program or script,
+ which prints a single line with a filename to standard output:
-->
- <shuffle>0</shuffle>
+ <playlist_program>1</playlist_program>
<!--
The following settings are used to describe your stream to the server.
It's up to you to make sure that the bitrate/samplerate/channels
Modified: trunk/ezstream/conf/ezstream_reencoding_example_vorbis.xml
===================================================================
--- trunk/ezstream/conf/ezstream_reencoding_example_vorbis.xml 2007-02-28 15:35:07 UTC (rev 12590)
+++ trunk/ezstream/conf/ezstream_reencoding_example_vorbis.xml 2007-02-28 21:26:16 UTC (rev 12591)
@@ -18,6 +18,11 @@
<!-- Enable playlist shuffling: -->
<shuffle>1</shuffle>
<!--
+ The file in <filename> is a regular playlist and not a program.
+ For demonstrational purposes, explicitly state this here:
+ -->
+ <playlist_program>0</playlist_program>
+ <!--
The following settings are used to describe your stream to the server.
It's up to you to make sure that the bitrate/quality/samplerate/channels
information matches up with your oggenc encoder settings below.
Modified: trunk/ezstream/doc/ezstream.1.in
===================================================================
--- trunk/ezstream/doc/ezstream.1.in 2007-02-28 15:35:07 UTC (rev 12590)
+++ trunk/ezstream/doc/ezstream.1.in 2007-02-28 21:26:16 UTC (rev 12591)
@@ -106,7 +106,7 @@
It contains all other configuration elements.
.El
.Ss Global configuration elements
-Each of the global configuration elements have the \&<ezstream\ /\&> element as
+Each of the global configuration elements have the \&<ezstream/\&> element as
their parent.
.Bl -ohang
.It Sy \&<url\ /\&>
@@ -143,7 +143,10 @@
to simply pass through the data, which may or may not work.
.It Sy \&<filename\ /\&>
.Pq Mandatory.
-Set the path and name of a single media file, a playlist or the keyword
+Set the path and name of a single media file, a playlist, the name of an
+external program
+.Pq see below ,
+or the keyword
.Pa stdin
for streaming from standard input.
Playlists are recognized by their filename extension and end with either
@@ -156,16 +159,24 @@
.Sq Li #
sign at the beginning of a line and ignored by
.Nm .
+.It Sy \&<playlist_program\ /\&>
+Indicates that the file in \&<filename/\&> is actually an executable program
+or script.
+This program is supposed to print
+.Pq to standard output
+one line with the name of a file that should be streamed next and then exit.
.It Sy \&<shuffle\ /\&>
.Pq Optional.
Set to
.Sy 1
.Pq one
-to randomly shuffle the entries of the playlist specified in \&<filename\ /\&>.
+to randomly shuffle the entries of the playlist specified in \&<filename/\&>.
Files are played sequentially if set to
.Sy 0
.Pq zero
-or when the \&<shuffle\ /\&> element is absent.
+or when the \&<shuffle/\&> element is absent.
+This option will be ignored if \&<playlist_program/\&> is set to 1
+.Pq one.
.It Sy \&<svrinfoname\ /\&>
.Pq Optional.
Set the name of the broadcast.
@@ -216,14 +227,14 @@
.Sy 0
.Pq zero ,
the Icecast server will not submit this stream to a YP directory, which is also
-the default if the \&<svrinfopublic\ /\&> element is absent.
+the default if the \&<svrinfopublic/\&> element is absent.
.It Sy \&<reencode\ /\&>
.Pq Optional.
Element that contains child elements, which specify if and how reencoding
should be done.
.El
.Ss Reencoding settings
-Each of the reencoding configuration elements have the \&<reencode\ /\&>
+Each of the reencoding configuration elements have the \&<reencode/\&>
element as their parent.
.Bl -ohang
.It Sy \&<enable\ /\&>
@@ -234,22 +245,22 @@
If set to
.Sy 0
.Pq zero ,
-no reencoding will be done, which is also the default if the \&<enable\ /\&>
+no reencoding will be done, which is also the default if the \&<enable/\&>
element is absent.
.It Sy \&<encdec\ /\&>
Element that contains child elements, which specify how to decode and encode
a certain media file format for streaming.
-Each format is described by a separate \&<encdec\ /\&> element.
+Each format is described by a separate \&<encdec/\&> element.
.El
.Ss Dencoder/Encoder settings
-Each of the decoder/encoder configuration elements have the \&<encdec\ /\&>
+Each of the decoder/encoder configuration elements have the \&<encdec/\&>
element as their parent.
.Bl -ohang
.It Sy \&<format\ /\&>
This element is used by
.Nm
to find the appropriate encoder for the output stream format specified in the
-\&<format\ /\&> element inside the global configuration.
+\&<format/\&> element inside the global configuration.
It is recommended that this element is always supplied, even for currently
unsupported output formats, with content such as
.Sy VORBIS ,
@@ -275,13 +286,13 @@
During runtime, the placeholder
.Sq Li @T@
is replaced with the fully qualified name of the media file, as specified in
-the \&<filename\ /\&> element or a playlist file.
+the \&<filename/\&> element or a playlist file.
It should always be enclosed in quotes, to prevent problems with filenames that
contain whitespaces.
.Pp
The metadata placeholder,
.Sq @M@ ,
-is also available in the \&<decode\ /\&> element.
+is also available in the \&<decode/\&> element.
That way it can be used for combined de-/encoder programs that produce readily
streamable data.
.Pp
Modified: trunk/ezstream/src/configfile.c
===================================================================
--- trunk/ezstream/src/configfile.c 2007-02-28 15:35:07 UTC (rev 12590)
+++ trunk/ezstream/src/configfile.c 2007-02-28 21:26:16 UTC (rev 12591)
@@ -140,7 +140,7 @@
xmlDocPtr doc;
xmlNodePtr cur;
char *ls_xmlContentPtr;
- int shuffle_set, svrinfopublic_set;
+ int shuffle_set, svrinfopublic_set, program_set;
xmlLineNumbersDefault(1);
if ((doc = xmlParseFile(fileName)) == NULL) {
@@ -160,6 +160,7 @@
shuffle_set = 0;
svrinfopublic_set = 0;
+ program_set = 0;
cur = cur->xmlChildrenNode;
while (cur != NULL) {
if (!xmlStrcmp(cur->name, BAD_CAST "url")) {
@@ -219,6 +220,22 @@
xmlFree(ls_xmlContentPtr);
}
}
+ if (!xmlStrcmp(cur->name, BAD_CAST "playlist_program")) {
+ if (program_set) {
+ printf("%s[%ld]: Error: Cannot have multiple <playlist_program> elements.\n",
+ fileName, xmlGetLineNo(cur));
+ goto config_error;
+ }
+ if (cur->xmlChildrenNode != NULL) {
+ int tmp;
+
+ ls_xmlContentPtr = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+ tmp = atoi(ls_xmlContentPtr);
+ ezConfig.fileNameIsProgram = (tmp == 0) ? 0 : 1;
+ xmlFree(ls_xmlContentPtr);
+ program_set = 1;
+ }
+ }
if (!xmlStrcmp(cur->name, BAD_CAST "shuffle")) {
if (shuffle_set) {
printf("%s[%ld]: Error: Cannot have multiple <shuffle> elements.\n",
Modified: trunk/ezstream/src/configfile.h
===================================================================
--- trunk/ezstream/src/configfile.h 2007-02-28 15:35:07 UTC (rev 12590)
+++ trunk/ezstream/src/configfile.h 2007-02-28 21:26:16 UTC (rev 12591)
@@ -57,6 +57,7 @@
FORMAT_ENCDEC *encoderDecoders[MAX_FORMAT_ENCDEC];
int numEncoderDecoders;
int shuffle;
+ int fileNameIsProgram;
} EZCONFIG;
EZCONFIG * getEZConfig(void);
Modified: trunk/ezstream/src/ezstream.c
===================================================================
--- trunk/ezstream/src/ezstream.c 2007-02-28 15:35:07 UTC (rev 12590)
+++ trunk/ezstream/src/ezstream.c 2007-02-28 21:26:16 UTC (rev 12591)
@@ -550,7 +550,7 @@
{
unsigned char buff[4096];
size_t read, total, oldTotal;
- int retval = 0;
+ int ret = 0;
#ifdef HAVE_GETTIMEOFDAY
double kbps = -1.0;
struct timeval timeStamp, *startTime = (struct timeval *)tv;
@@ -567,21 +567,21 @@
total = oldTotal = 0;
while ((read = fread(buff, 1, sizeof(buff), filepstream)) > 0) {
- int ret;
-
if (rereadPlaylist_notify) {
rereadPlaylist_notify = 0;
- printf("%s: SIGHUP signal received, will reread playlist after this file.\n",
- __progname);
+ if (!pezConfig->fileNameIsProgram)
+ printf("%s: SIGHUP signal received, will reread playlist after this file.\n",
+ __progname);
}
if (skipTrack) {
skipTrack = 0;
- retval = 2;
+ ret = 2;
break;
}
- ret = shout_send(shout, buff, read);
- if (ret != SHOUTERR_SUCCESS) {
+ shout_sync(shout);
+
+ if (shout_send(shout, buff, read) != SHOUTERR_SUCCESS) {
printf("%s: shout_send(): %s\n", __progname,
shout_get_error(shout));
while (1) {
@@ -591,13 +591,11 @@
if (shout_open(shout) == SHOUTERR_SUCCESS) {
printf("%s: Reconnect to server successful.\n",
__progname);
- ret = shout_send(shout, buff, read);
- if (ret != SHOUTERR_SUCCESS)
- printf("%s: shout_send(): %s\n",
- __progname,
- shout_get_error(shout));
- else
+ if (shout_send(shout, buff, read) == SHOUTERR_SUCCESS)
break;
+ printf("%s: shout_send(): %s\n",
+ __progname,
+ shout_get_error(shout));
} else {
printf("%s: Reconnect failed. Waiting 5 seconds ...\n",
__progname);
@@ -610,8 +608,6 @@
}
}
- shout_sync(shout);
-
total += read;
if (qFlag && vFlag) {
#ifdef HAVE_GETTIMEOFDAY
@@ -620,10 +616,17 @@
unsigned int hrs, mins, secs;
#endif /* HAVE_GETTIMEOFDAY */
- if (!isStdin && playlistMode)
- printf(" [%4lu/%-4lu]",
- playlist_get_position(playlist),
- playlist_get_num_items(playlist));
+ if (!isStdin && playlistMode) {
+ if (pezConfig->fileNameIsProgram) {
+ char *tmp = xstrdup(pezConfig->fileName);
+ printf(" [%s]",
+ basename(tmp));
+ xfree(tmp);
+ } else
+ printf(" [%4lu/%-4lu]",
+ playlist_get_position(playlist),
+ playlist_get_num_items(playlist));
+ }
#ifdef HAVE_GETTIMEOFDAY
oldTime = (double)timeStamp.tv_sec
@@ -656,13 +659,13 @@
if (ferror(filepstream)) {
if (errno == EINTR) {
clearerr(filepstream);
- retval = 1;
+ ret = 1;
} else
printf("%s: streamFile(): Error while reading '%s': %s\n",
__progname, fileName, strerror(errno));
}
- return (retval);
+ return (ret);
}
int
@@ -732,17 +735,23 @@
const char *song;
char lastSong[PATH_MAX + 1];
- /*
- * XXX: This preserves traditional behavior, however, rereading the
- * playlist after each walkthrough seems a bit more logical.
- */
if (playlist == NULL) {
- if ((playlist = playlist_read(fileName)) == NULL)
- return (0);
+ if (pezConfig->fileNameIsProgram) {
+ if ((playlist = playlist_program(fileName)) == NULL)
+ return (0);
+ } else {
+ if ((playlist = playlist_read(fileName)) == NULL)
+ return (0);
+ }
} else
+ /*
+ * XXX: This preserves traditional behavior, however,
+ * rereading the playlist after each walkthrough seems a
+ * bit more logical.
+ */
playlist_rewind(playlist);
- if (pezConfig->shuffle)
+ if (!pezConfig->fileNameIsProgram && pezConfig->shuffle)
playlist_shuffle(playlist);
while ((song = playlist_get_next(playlist)) != NULL) {
@@ -751,6 +760,8 @@
return (0);
if (rereadPlaylist) {
rereadPlaylist = rereadPlaylist_notify = 0;
+ if (pezConfig->fileNameIsProgram)
+ continue;
printf("%s: Rereading playlist\n", __progname);
if (!playlist_reread(&playlist))
return (0);
@@ -1095,13 +1106,18 @@
tmpFileName = xstrdup(pezConfig->fileName);
for (p = tmpFileName; *p != '\0'; p++)
*p = tolower((int)*p);
- if (strrcmp(tmpFileName, ".m3u") == 0 ||
+ if (pezConfig->fileNameIsProgram ||
+ strrcmp(tmpFileName, ".m3u") == 0 ||
strrcmp(tmpFileName, ".txt") == 0)
playlistMode = 1;
else
playlistMode = 0;
xfree(tmpFileName);
+ if (vFlag && pezConfig->fileNameIsProgram)
+ printf("%s: Using program '%s' to get filenames for streaming\n",
+ __progname, pezConfig->fileName);
+
ret = 1;
do {
if (playlistMode)
@@ -1114,6 +1130,9 @@
printf("%s: Connection to http://%s:%d%s failed: %s\n", __progname,
host, port, mount, shout_get_error(shout));
+ if (vFlag)
+ printf("%s: Exiting ...\n", __progname);
+
shout_close(shout);
playlist_free(playlist);
Modified: trunk/ezstream/src/playlist.c
===================================================================
--- trunk/ezstream/src/playlist.c 2007-02-28 15:35:07 UTC (rev 12590)
+++ trunk/ezstream/src/playlist.c 2007-02-28 21:26:16 UTC (rev 12591)
@@ -21,6 +21,9 @@
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
+#ifdef HAVE_SYS_STAT_H
+# include <sys/stat.h>
+#endif
#include <errno.h>
#include <limits.h>
#include <stdio.h>
@@ -33,6 +36,9 @@
#ifdef WIN32
# define snprintf _snprintf
+# define popen _popen
+# define pclose _pclose
+# define stat _stat
#endif
#ifndef SIZE_T_MAX
@@ -47,21 +53,24 @@
extern char *__progname;
struct playlist {
- char *filename;
- char **list;
- size_t size;
- size_t num;
- size_t index;
+ char *filename;
+ char **list;
+ size_t size;
+ size_t num;
+ size_t index;
+ int program;
+ char *prog_track;
};
playlist_t * playlist_create(const char *);
int playlist_add(playlist_t *, const char *);
unsigned int playlist_random(void);
+const char * playlist_run_program(playlist_t *);
playlist_t *
playlist_create(const char *filename)
{
- playlist_t *pl;
+ playlist_t *pl;
pl = xcalloc(1, sizeof(playlist_t));
pl->filename = xstrdup(filename);
@@ -145,16 +154,15 @@
playlist_t *
playlist_read(const char *filename)
{
- playlist_t *pl;
- unsigned long line;
- FILE *filep;
- char buf[PATH_MAX + 1];
+ playlist_t *pl;
+ unsigned long line;
+ FILE *filep;
+ char buf[PATH_MAX + 1];
pl = playlist_create(filename);
if ((filep = fopen(filename, "r")) == NULL) {
- printf("%s: playlist_read(): %s: %s\n", __progname, filename,
- strerror(errno));
+ printf("%s: %s: %s\n", __progname, filename, strerror(errno));
playlist_free(pl);
return (NULL);
}
@@ -213,6 +221,42 @@
return (pl);
}
+playlist_t *
+playlist_program(const char *filename)
+{
+ playlist_t *pl;
+#ifdef HAVE_STAT
+ struct stat st;
+#else
+ FILE *filep;
+#endif
+
+ pl = playlist_create(filename);
+ pl->program = 1;
+
+#ifdef HAVE_STAT
+ if (stat(filename, &st) == -1) {
+ printf("%s: %s: %s\n", __progname, filename, strerror(errno));
+ playlist_free(pl);
+ return (NULL);
+ }
+ if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
+ printf("%s: %s: Not an executable program\n", __progname, filename);
+ playlist_free(pl);
+ return (NULL);
+ }
+#else
+ if ((filep = fopen(filename, "r")) == NULL) {
+ printf("%s: %s: %s\n", __progname, filename, strerror(errno));
+ playlist_free(pl);
+ return (NULL);
+ }
+ fclose(filep);
+#endif /* HAVE_STAT */
+
+ return (pl);
+}
+
void
playlist_free(playlist_t *pl)
{
@@ -239,6 +283,9 @@
pl->list = NULL;
}
+ if (pl->prog_track != NULL)
+ xfree(pl->prog_track);
+
xfree(pl);
pl = NULL;
}
@@ -253,6 +300,9 @@
exit(1);
}
+ if (pl->program)
+ return (playlist_run_program(pl));
+
return ((const char *)pl->list[pl->index++]);
}
@@ -265,6 +315,9 @@
exit(1);
}
+ if (pl->program)
+ return (NULL);
+
return ((const char *)pl->list[pl->index]);
}
@@ -277,6 +330,9 @@
exit(1);
}
+ if (pl->program)
+ return;
+
if (pl->list[pl->index] != NULL)
pl->index++;
}
@@ -290,6 +346,9 @@
exit(1);
}
+ if (pl->program)
+ return (0);
+
return ((unsigned long)pl->num);
}
@@ -302,6 +361,9 @@
exit(1);
}
+ if (pl->program)
+ return (0);
+
return ((unsigned long)pl->index);
}
@@ -314,7 +376,7 @@
exit(1);
}
- if (index > pl->num - 1)
+ if (pl->program || index > pl->num - 1)
return (0);
pl->index = (size_t)index;
@@ -333,7 +395,7 @@
exit(1);
}
- if (pl->num == 0)
+ if (pl->program || pl->num == 0)
return (0);
for (i = 0; i < pl->num; i++) {
@@ -355,6 +417,9 @@
exit(1);
}
+ if (pl->program)
+ return;
+
pl->index = 0;
}
@@ -371,6 +436,9 @@
pl = *plist;
+ if (pl->program)
+ return (0);
+
if ((new_pl = playlist_read(pl->filename)) == NULL)
return (0);
@@ -386,8 +454,8 @@
void
playlist_shuffle(playlist_t *pl)
{
- unsigned long d, i, range;
- char *temp;
+ unsigned long d, i, range;
+ char *temp;
if (pl == NULL) {
printf("%s: playlist_shuffle(): Internal error: NULL argument\n",
@@ -395,7 +463,7 @@
exit(1);
}
- if (pl->num < 2)
+ if (pl->program || pl->num < 2)
return;
for (i = 0; i < pl->num; i++) {
@@ -420,3 +488,66 @@
pl->list[i] = temp;
}
}
+
+const char *
+playlist_run_program(playlist_t *pl)
+{
+ FILE *filep;
+ char buf[PATH_MAX + 1];
+
+ if (pl == NULL) {
+ printf("%s: playlist_run_program(): Internal error: NULL argument\n",
+ __progname);
+ exit(1);
+ }
+
+ if (!pl->program)
+ return (NULL);
+
+ fflush(NULL);
+ errno = 0;
+ if ((filep = popen(pl->filename, "r")) == NULL) {
+ printf("%s: playlist_run_program(): Error while executing '%s'",
+ __progname, pl->filename);
+ /* popen() does not set errno reliably ... */
+ if (errno)
+ printf(": %s\n", strerror(errno));
+ else
+ printf("\n");
+ return (NULL);
+ }
+
+ if (fgets(buf, sizeof(buf), filep) == NULL) {
+ if (ferror(filep))
+ printf("%s: Error while reading output from program '%s': %s\n",
+ __progname, pl->filename, strerror(errno));
+ pclose(filep);
+ printf("%s: FATAL: External program '%s' not (or no longer) usable.\n",
+ __progname, pl->filename);
+ exit(1);
+ }
+
+ pclose(filep);
+
+ if (strlen(buf) == sizeof(buf) - 1) {
+ printf("%s: Output from program '%s' too long\n", __progname,
+ pl->filename);
+ return (NULL);
+ }
+
+ if (buf[0] != '\0' && buf[strlen(buf) - 1] == '\n')
+ buf[strlen(buf) - 1] = '\0';
+ if (buf[0] != '\0' && buf[strlen(buf) - 1] == '\r')
+ buf[strlen(buf) - 1] = '\0';
+ if (buf[0] == '\0') {
+ printf("%s: Empty line received from program '%s'\n",
+ __progname, pl->filename);
+ return (NULL);
+ }
+
+ if (pl->prog_track != NULL)
+ xfree(pl->prog_track);
+ pl->prog_track = xstrdup(buf);
+
+ return ((const char *)pl->prog_track);
+}
Modified: trunk/ezstream/src/playlist.h
===================================================================
--- trunk/ezstream/src/playlist.h 2007-02-28 15:35:07 UTC (rev 12590)
+++ trunk/ezstream/src/playlist.h 2007-02-28 21:26:16 UTC (rev 12591)
@@ -37,6 +37,13 @@
playlist_t * playlist_read(const char * /* filename */);
/*
+ * For each call to playlist_get_next(), the specified program is run. This
+ * program is supposed to print one line to standard output, containing the
+ * path and filename of a media file.
+ */
+playlist_t * playlist_program(const char * /* program name */);
+
+/*
* Free all memory used by a playlist handler that was created with
* playlist_read().
*/
@@ -49,6 +56,11 @@
const char * playlist_get_next(playlist_t *);
/*
+ * The functions below work on playlist handlers obtained with playlist_read()
+ * and are no-ops (i.e. return failure) for playlists from playlist_program().
+ */
+
+/*
* Get the next item in the playlist without moving on to the following entry.
* Returns a NUL-terminated string of the next playlist entry, or NULL if the
* currently playing song is the last one in the list.
More information about the commits
mailing list