commit - a72597083f64cda4978a3bce3eb4dba6ae2b6d32
commit + ce1bfad9f1f7c6b1599e0bfed0a1b169d4a30781
blob - 2b1255118b91d4c7edef0a6016130ed9de07bfcd
blob + 385ea8c6903bd7108ab3f4de6767bc377bb43dba
--- gitwrapper/gitwrapper.c
+++ gitwrapper/gitwrapper.c
goto done;
}
- repo = gotd_find_repo_by_name(repo_name, &gotd);
+ repo = gotd_find_repo_by_name(repo_name, &gotd.repos);
/*
* Invoke our custom Git server if the repository was found
blob - 06930323b2023b42abe628c2dfe3bc4a12762e70
blob + 8ba95cce4988f25eb3c716683e8c3e1952f930e6
--- gotd/gotd.c
+++ gotd/gotd.c
#include "got_repository.h"
#include "got_object.h"
#include "got_reference.h"
+#include "got_diff.h"
#include "got_lib_delta.h"
#include "got_lib_object.h"
#include "session.h"
#include "repo_read.h"
#include "repo_write.h"
+#include "notify.h"
#ifndef nitems
#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
disconnect(client);
}
+ if (proc == gotd.notify_proc)
+ gotd.notify_proc = NULL;
+
evtimer_del(&proc->tmo);
if (proc->iev.ibuf.fd != -1) {
err = ensure_client_is_not_writing(client);
if (err)
return err;
- repo = gotd_find_repo_by_name(ireq.repo_name, &gotd);
+ repo = gotd_find_repo_by_name(ireq.repo_name, &gotd.repos);
if (repo == NULL)
return got_error(GOT_ERR_NOT_GIT_REPO);
err = start_auth_child(client, GOTD_AUTH_READ, repo,
err = ensure_client_is_not_reading(client);
if (err)
return err;
- repo = gotd_find_repo_by_name(ireq.repo_name, &gotd);
+ repo = gotd_find_repo_by_name(ireq.repo_name, &gotd.repos);
if (repo == NULL)
return got_error(GOT_ERR_NOT_GIT_REPO);
err = start_auth_child(client,
"session_write",
"repo_read",
"repo_write",
- "gitwrapper"
+ "gitwrapper",
+ "notify"
};
static void
}
static void
+gotd_dispatch_notifier(int fd, short event, void *arg)
+{
+ struct gotd_imsgev *iev = arg;
+ struct imsgbuf *ibuf = &iev->ibuf;
+ struct gotd_child_proc *proc = gotd.notify_proc;
+ ssize_t n;
+ int shut = 0;
+ struct imsg imsg;
+
+ if (proc->iev.ibuf.fd != fd)
+ fatalx("%s: unexpected fd %d", __func__, fd);
+
+ if (event & EV_READ) {
+ if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+ fatal("imsg_read error");
+ if (n == 0) {
+ /* Connection closed. */
+ shut = 1;
+ goto done;
+ }
+ }
+
+ if (event & EV_WRITE) {
+ n = msgbuf_write(&ibuf->w);
+ if (n == -1 && errno != EAGAIN)
+ fatal("msgbuf_write");
+ if (n == 0) {
+ /* Connection closed. */
+ shut = 1;
+ goto done;
+ }
+ }
+
+ for (;;) {
+ if ((n = imsg_get(ibuf, &imsg)) == -1)
+ fatal("%s: imsg_get error", __func__);
+ if (n == 0) /* No more messages. */
+ break;
+
+ switch (imsg.hdr.type) {
+ default:
+ log_debug("unexpected imsg %d", imsg.hdr.type);
+ break;
+ }
+
+ imsg_free(&imsg);
+ }
+done:
+ if (!shut) {
+ gotd_imsg_event_add(iev);
+ } else {
+ /* This pipe is dead. Remove its event handler */
+ event_del(&iev->ev);
+
+ /*
+ * Do not exit all of gotd if the notification handler dies.
+ * We can continue operating without notifications until an
+ * operator intervenes.
+ */
+ log_warnx("notify child process (pid %d) closed its imsg pipe "
+ "unexpectedly", proc->pid);
+ proc_done(proc);
+ }
+}
+
+static void
gotd_dispatch_auth_child(int fd, short event, void *arg)
{
const struct got_error *err = NULL;
goto done;
}
- repo = gotd_find_repo_by_name(client->auth->repo_name, &gotd);
+ repo = gotd_find_repo_by_name(client->auth->repo_name, &gotd.repos);
if (repo == NULL) {
err = got_error(GOT_ERR_NOT_GIT_REPO);
goto done;
const struct got_error *err = NULL;
struct gotd_imsg_connect iconnect;
int s;
+ struct ibuf *wbuf;
memset(&iconnect, 0, sizeof(iconnect));
iconnect.client_id = client->id;
iconnect.euid = client->euid;
iconnect.egid = client->egid;
+ iconnect.username_len = strlen(client->username);
- if (gotd_imsg_compose_event(&client->session->iev, GOTD_IMSG_CONNECT,
- PROC_GOTD, s, &iconnect, sizeof(iconnect)) == -1) {
+ wbuf = imsg_create(&client->session->iev.ibuf, GOTD_IMSG_CONNECT,
+ PROC_GOTD, gotd.pid, sizeof(iconnect) + iconnect.username_len);
+ if (wbuf == NULL) {
err = got_error_from_errno("imsg compose CONNECT");
close(s);
return err;
}
+ if (imsg_add(wbuf, &iconnect, sizeof(iconnect)) == -1) {
+ close(s);
+ return got_error_from_errno("imsg_add CONNECT");
+ }
+ if (imsg_add(wbuf, client->username, iconnect.username_len) == -1) {
+ close(s);
+ return got_error_from_errno("imsg_add CONNECT");
+ }
+ ibuf_fd_set(wbuf, s);
+ imsg_close(&client->session->iev.ibuf, wbuf);
+ gotd_imsg_event_add(&client->session->iev);
+
/*
* We are no longer interested in messages from this client.
* Further client requests will be handled by the session process.
struct gotd_repo *repo;
const char *name = client->session->repo_name;
- repo = gotd_find_repo_by_name(name, &gotd);
+ repo = gotd_find_repo_by_name(name, &gotd.repos);
if (repo != NULL) {
enum gotd_procid proc_type;
}
}
+static const struct got_error *
+connect_notifier_and_session(struct gotd_client *client)
+{
+ const struct got_error *err = NULL;
+ struct gotd_imsgev *session_iev = &client->session->iev;
+ int pipe[2];
+
+ if (gotd.notify_proc == NULL)
+ return NULL;
+
+ if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
+ PF_UNSPEC, pipe) == -1)
+ return got_error_from_errno("socketpair");
+
+ /* Pass notifier pipe to session . */
+ if (gotd_imsg_compose_event(session_iev, GOTD_IMSG_CONNECT_NOTIFIER,
+ PROC_GOTD, pipe[0], NULL, 0) == -1) {
+ err = got_error_from_errno("imsg compose CONNECT_NOTIFIER");
+ close(pipe[0]);
+ close(pipe[1]);
+ return err;
+ }
+
+ /* Pass session pipe to notifier. */
+ if (gotd_imsg_compose_event(&gotd.notify_proc->iev,
+ GOTD_IMSG_CONNECT_SESSION, PROC_GOTD, pipe[1], NULL, 0) == -1) {
+ err = got_error_from_errno("imsg compose CONNECT_SESSION");
+ close(pipe[1]);
+ return err;
+ }
+
+ return NULL;
+}
+
static void
gotd_dispatch_repo_child(int fd, short event, void *arg)
{
err = connect_session(client);
if (err)
break;
+ err = connect_notifier_and_session(client);
+ if (err)
+ break;
err = connect_repo_child(client, proc);
break;
default:
case PROC_REPO_WRITE:
argv[argc++] = (char *)"-W";
break;
+ case PROC_NOTIFY:
+ argv[argc++] = (char *)"-N";
+ break;
default:
fatalx("invalid process id %d", proc_id);
}
gotd.listen_proc = proc;
}
+static void
+start_notifier(char *argv0, const char *confpath, int daemonize, int verbosity)
+{
+ struct gotd_child_proc *proc;
+
+ proc = calloc(1, sizeof(*proc));
+ if (proc == NULL)
+ fatal("calloc");
+
+ TAILQ_INSERT_HEAD(&procs, proc, entry);
+
+ /* proc->tmo is initialized in main() after event_init() */
+
+ proc->type = PROC_NOTIFY;
+
+ if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
+ PF_UNSPEC, proc->pipe) == -1)
+ fatal("socketpair");
+
+ proc->pid = start_child(proc->type, NULL, argv0, confpath,
+ proc->pipe[1], daemonize, verbosity);
+ imsg_init(&proc->iev.ibuf, proc->pipe[0]);
+ proc->iev.handler = gotd_dispatch_notifier;
+ proc->iev.events = EV_READ;
+ proc->iev.handler_arg = NULL;
+ event_set(&proc->iev.ev, proc->iev.ibuf.fd, EV_READ,
+ gotd_dispatch_notifier, &proc->iev);
+
+ gotd.notify_proc = proc;
+}
+
static const struct got_error *
start_session_child(struct gotd_client *client, struct gotd_repo *repo,
char *argv0, const char *confpath, int daemonize, int verbosity)
setrlimit(RLIMIT_DATA, &rl);
}
+static void
+unveil_notification_helpers(void)
+{
+ const char *helpers[] = {
+ GOTD_PATH_PROG_NOTIFY_EMAIL,
+ GOTD_PATH_PROG_NOTIFY_HTTP,
+ };
+ size_t i;
+
+ for (i = 0; i < nitems(helpers); i++) {
+ if (unveil(helpers[i], "x") == 0)
+ continue;
+ fatal("unveil %s", helpers[i]);
+ }
+
+ if (unveil(NULL, NULL) == -1)
+ fatal("unveil");
+}
+
int
main(int argc, char **argv)
{
struct event evsigint, evsigterm, evsighup, evsigusr1, evsigchld;
int *pack_fds = NULL, *temp_fds = NULL;
struct gotd_repo *repo = NULL;
+ char *default_sender = NULL;
+ char hostname[HOST_NAME_MAX + 1];
+ FILE *diff_f1 = NULL, *diff_f2 = NULL;
+ int diff_fd1 = -1, diff_fd2 = -1;
TAILQ_INIT(&procs);
log_init(1, LOG_DAEMON); /* Log to stderr until daemonized. */
- while ((ch = getopt(argc, argv, "Adf:LnP:RsSvW")) != -1) {
+ while ((ch = getopt(argc, argv, "Adf:LnNP:RsSvW")) != -1) {
switch (ch) {
case 'A':
proc_id = PROC_AUTH;
case 'n':
noaction = 1;
break;
+ case 'N':
+ proc_id = PROC_NOTIFY;
+ break;
case 'P':
repo_path = realpath(optarg, NULL);
if (repo_path == NULL)
fatal("daemon");
gotd.pid = getpid();
start_listener(argv0, confpath, daemonize, verbosity);
+ start_notifier(argv0, confpath, daemonize, verbosity);
} else if (proc_id == PROC_LISTEN) {
snprintf(title, sizeof(title), "%s", gotd_proc_names[proc_id]);
if (verbosity) {
fatalx("repository path not specified");
snprintf(title, sizeof(title), "%s %s",
gotd_proc_names[proc_id], repo_path);
+ } else if (proc_id == PROC_NOTIFY) {
+ snprintf(title, sizeof(title), "%s", gotd_proc_names[proc_id]);
+ if (gethostname(hostname, sizeof(hostname)) == -1)
+ fatal("gethostname");
+ if (asprintf(&default_sender, "%s@%s",
+ pw->pw_name, hostname) == -1)
+ fatal("asprintf");
} else
fatal("invalid process id %d", proc_id);
#endif
if (proc_id == PROC_SESSION_READ)
apply_unveil_repo_readonly(repo_path, 1);
- else
+ else {
apply_unveil_repo_readwrite(repo_path);
-
+ repo = gotd_find_repo_by_path(repo_path, &gotd);
+ if (repo == NULL)
+ fatalx("no repository for path %s", repo_path);
+ }
session_main(title, repo_path, pack_fds, temp_fds,
- &gotd.request_timeout, proc_id);
+ &gotd.request_timeout, repo, proc_id);
/* NOTREACHED */
break;
case PROC_REPO_READ:
exit(0);
case PROC_REPO_WRITE:
set_max_datasize();
+
+ diff_f1 = got_opentemp();
+ if (diff_f1 == NULL)
+ fatal("got_opentemp");
+ diff_f2 = got_opentemp();
+ if (diff_f2 == NULL)
+ fatal("got_opentemp");
+ diff_fd1 = got_opentempfd();
+ if (diff_fd1 == -1)
+ fatal("got_opentempfd");
+ diff_fd2 = got_opentempfd();
+ if (diff_fd2 == -1)
+ fatal("got_opentempfd");
#ifndef PROFILE
if (pledge("stdio rpath recvfd unveil", NULL) == -1)
err(1, "pledge");
drop_privs(pw);
repo_write_main(title, repo_path, pack_fds, temp_fds,
+ diff_f1, diff_f2, diff_fd1, diff_fd2,
&repo->protected_tag_namespaces,
&repo->protected_branch_namespaces,
&repo->protected_branches);
/* NOTREACHED */
exit(0);
+ case PROC_NOTIFY:
+#ifndef PROFILE
+ if (pledge("stdio proc exec recvfd unveil", NULL) == -1)
+ err(1, "pledge");
+#endif
+ /*
+ * Limit "exec" promise to notification helpers via unveil(2).
+ */
+ unveil_notification_helpers();
+
+ notify_main(title, &gotd.repos, default_sender);
+ /* NOTREACHED */
+ exit(0);
default:
fatal("invalid process id %d", proc_id);
}
evtimer_set(&gotd.listen_proc->tmo, kill_proc_timeout,
gotd.listen_proc);
+ if (gotd.notify_proc) {
+ evtimer_set(&gotd.notify_proc->tmo, kill_proc_timeout,
+ gotd.notify_proc);
+ }
apply_unveil_selfexec();
signal_add(&evsigchld, NULL);
gotd_imsg_event_add(&gotd.listen_proc->iev);
+ if (gotd.notify_proc)
+ gotd_imsg_event_add(&gotd.notify_proc->iev);
event_dispatch();
free(repo_path);
+ free(default_sender);
gotd_shutdown();
return 0;
blob - 09928aa29395cb1acfaff6303c26cda1adfaf34a
blob + 45a21b3bd385d22ec6ae514d311b7bd4a6753183
--- gotd/gotd.conf.5
+++ gotd/gotd.conf.5
.Nm .
These namespaces are always protected and even attempts to create new
references in these namespaces will always be denied.
+.It Ic notify Brq Ar ...
+The
+.Ic notify
+directive enables notifications about new commits or tags
+added to the repository.
+.Pp
+Notifications via email require an SMTP daemon which accepts mail
+for forwarding without requiring client authentication or encryption.
+On
+.Ox
+the
+.Xr smtpd 8
+daemon can be used for this purpose.
+The default content of email notifications looks similar to the output of the
+.Cm got log -d
+command.
+.Pp
+.\" Notifications via HTTP require a HTTP or HTTPS server which is accepting
+.\" POST requests with or without HTTP Basic authentication.
+.\" Depending on the use case a custom server-side CGI script may be required
+.\" for the processing of notifications.
+.\" HTTP notifications can achieve functionality
+.\" similar to Git's server-side post-receive hook script with
+.\" .Xr gotd 8
+.\" by triggering arbitrary post-commit actions via the HTTP server.
+.\" .Pp
+The
+.Ic notify
+directive expects parameters which must be enclosed in curly braces.
+The available parameters are as follows:
+.Pp
+.Bl -tag -width Ds
+.It Ic branch Ar name
+Send notifications about commits to the named branch.
+The
+.Ar name
+will be looked up in the
+.Dq refs/heads/
+reference namespace.
+This directive may be specified multiple times to build a list of
+branches to send notifications for.
+If neither a
+.Ic branch
+nor a
+.Ic reference namespace
+are specified then changes to any reference will trigger notifications.
+.It Ic reference Ic namespace Ar namespace
+Send notifications about commits or tags within a reference namespace.
+This directive may be specified multiple times to build a list of
+namespaces to send notifications for.
+If neither a
+.Ic branch
+nor a
+.Ic reference namespace
+are specified then changes to any reference will trigger notifications.
+.It Ic email Oo Ic from Ar sender Oc Ic to Ar recipient Oo Ic reply to Ar responder Oc Oo Ic relay Ar hostname Oo Ic port Ar port Oc Oc
+Send notifications via email to the specified
+.Ar recipient .
+This directive may be specified multiple times to build a list of
+recipients to send notifications to.
+.Pp
+The
+.Ar recipient
+must be an email addresses that accepts mail.
+The
+.Ar sender
+will be used as the From address.
+If not specified, the sender defaults to an email address composed of the user
+account running
+.Xr gotd 8
+and the local hostname.
+.Pp
+If a
+.Ar responder
+is specified via the
+.Ic reply to
+directive, the
+.Ar responder
+will be used as the Reply-to address.
+Setting the Reply-to header can be useful if replies should go to a
+mailing list instead of the
+.Ar sender ,
+for example.
+.Pp
+By default, mail will be sent to the SMTP server listening on the loopback
+address 127.0.0.1 on port 25.
+The
+.Ic relay
+and
+.Ic port
+directives can be used to specify a different SMTP server address and port.
+.Pp
+.\" .It Ic url Ar URL Ic user Ar user Ic password Ar password Oc
+.\" Send notifications via HTTP.
+.\" This directive may be specified multiple times to build a list of
+.\" HTTP servers to send notifications to.
+.\" .Pp
+.\" The notification will be sent as a POST request to the given
+.\" .Ar URL ,
+.\" which must be a valid HTTP URL and begin with either
+.\" .Dq http://
+.\" or
+.\" .Dq https:// .
+.\" If HTTPS is used, sending of notifications will only succeed if
+.\" no TLS errors occur.
+.\" .Pp
+.\" The optional
+.\" .Ic user
+.\" and
+.\" .Ic password
+.\" directives enable HTTP Basic authentication.
+.\" If used, both a
+.\" .Ar user
+.\" and a
+.\" .Ar password
+.\" must be specified.
+.\" The
+.\" .Ar password
+.\" must not be an empty string.
+.\" .Pp
+.\" The request body contains a JSON document with the following objects:
+.\" .Bl -tag -width { "notifications" : array }
+.\" .It { "notifications" : array }
+.\" The top-level object contains an array of all notifications in this request.
+.\" .It TODO ...
+.\" .El
.El
.Sh FILES
.Bl -tag -width Ds -compact
branch "main"
tag namespace "refs/tags/"
}
+
+ notify {
+ branch "main"
+ reference namespace "refs/tags/"
+ email to openbsd-ports-changes@example.com
+.\" url https://example.com/notify/ user "flan_announcer" password "secret"
+ }
}
# Use a larger request timeout value:
blob - c9dacfdc7fc166370fa8e6384c7fef0a6ddc3fac
blob + 5d41c4eb56cc8a5acf41912a51075953aa9f3e79
--- gotd/gotd.h
+++ gotd/gotd.h
#define GOTD_EMPTY_PATH "/var/empty"
#endif
+#ifndef GOT_LIBEXECDIR
+#define GOT_LIBEXECDIR /usr/libexec
+#endif
+
+#define GOTD_STRINGIFY(x) #x
+#define GOTD_STRINGVAL(x) GOTD_STRINGIFY(x)
+
+#define GOTD_PROG_NOTIFY_EMAIL got-notify-email
+#define GOTD_PROG_NOTIFY_HTTP got-notify-http
+
+#define GOTD_PATH_PROG_NOTIFY_EMAIL \
+ GOTD_STRINGVAL(GOT_LIBEXECDIR) "/" \
+ GOTD_STRINGVAL(GOTD_PROG_NOTIFY_EMAIL)
+#define GOTD_PATH_PROG_NOTIFY_HTTP \
+ GOTD_STRINGVAL(GOT_LIBEXECDIR) "/" \
+ GOTD_STRINGVAL(GOTD_PROG_NOTIFY_HTTP)
+
#define GOTD_MAXCLIENTS 1024
#define GOTD_MAX_CONN_PER_UID 4
#define GOTD_FD_RESERVE 5
PROC_REPO_READ,
PROC_REPO_WRITE,
PROC_GITWRAPPER,
+ PROC_NOTIFY,
PROC_MAX,
};
char *identifier;
};
STAILQ_HEAD(gotd_access_rule_list, gotd_access_rule);
+
+enum gotd_notification_target_type {
+ GOTD_NOTIFICATION_VIA_EMAIL,
+ GOTD_NOTIFICATION_VIA_HTTP
+};
+
+struct gotd_notification_target {
+ STAILQ_ENTRY(gotd_notification_target) entry;
+
+ enum gotd_notification_target_type type;
+ union {
+ struct {
+ char *sender;
+ char *recipient;
+ char *responder;
+ char *hostname;
+ char *port;
+ } email;
+ struct {
+ char *url;
+ char *user;
+ char *password;
+ } http;
+ } conf;
+};
+STAILQ_HEAD(gotd_notification_targets, gotd_notification_target);
struct gotd_repo {
TAILQ_ENTRY(gotd_repo) entry;
struct got_pathlist_head protected_tag_namespaces;
struct got_pathlist_head protected_branch_namespaces;
struct got_pathlist_head protected_branches;
+
+ struct got_pathlist_head notification_refs;
+ struct got_pathlist_head notification_ref_namespaces;
+ struct gotd_notification_targets notification_targets;
};
TAILQ_HEAD(gotd_repolist, gotd_repo);
GOTD_STATE_EXPECT_PACKFILE,
GOTD_STATE_EXPECT_DONE,
GOTD_STATE_DONE,
+ GOTD_STATE_NOTIFY,
};
struct gotd_client_capability {
struct gotd_repolist repos;
int nrepos;
struct gotd_child_proc *listen_proc;
+ struct gotd_child_proc *notify_proc;
+ int notifications_enabled;
struct timeval request_timeout;
struct timeval auth_timeout;
struct gotd_uid_connection_limit *connection_limits;
/* Auth child process. */
GOTD_IMSG_AUTHENTICATE,
GOTD_IMSG_ACCESS_GRANTED,
+
+ /* Notify child process. */
+ GOTD_IMSG_CONNECT_NOTIFIER,
+ GOTD_IMSG_CONNECT_SESSION,
+ GOTD_IMSG_NOTIFY,
+ GOTD_IMSG_NOTIFICATION_SENT
};
/* Structure for GOTD_IMSG_ERROR. */
uint32_t client_id;
uid_t euid;
gid_t egid;
+ size_t username_len;
+
+ /* Followed by username_len data bytes. */
};
/* Structure for GOTD_IMSG_CONNECT_REPO_CHILD. */
uint32_t client_id;
};
-int enter_chroot(const char *);
+/* Structures for GOTD_IMSG_NOTIFY. */
+enum gotd_notification_action {
+ GOTD_NOTIF_ACTION_CREATED,
+ GOTD_NOTIF_ACTION_REMOVED,
+ GOTD_NOTIF_ACTION_CHANGED
+};
+/* IMSG_NOTIFY session <-> repo_write */
+struct gotd_imsg_notification_content {
+ uint32_t client_id;
+ enum gotd_notification_action action;
+ uint8_t old_id[SHA1_DIGEST_LENGTH];
+ uint8_t new_id[SHA1_DIGEST_LENGTH];
+ size_t refname_len;
+ /* Followed by refname_len data bytes. */
+};
+/* IMSG_NOTIFY session -> notify*/
+struct gotd_imsg_notify {
+ char repo_name[NAME_MAX];
+ char subject_line[64];
+};
+
int parse_config(const char *, enum gotd_procid, struct gotd *);
-struct gotd_repo *gotd_find_repo_by_name(const char *, struct gotd *);
+struct gotd_repo *gotd_find_repo_by_name(const char *, struct gotd_repolist *);
struct gotd_repo *gotd_find_repo_by_path(const char *, struct gotd *);
struct gotd_uid_connection_limit *gotd_find_uid_connection_limit(
struct gotd_uid_connection_limit *limits, size_t nlimits, uid_t uid);
int gotd_parseuid(const char *s, uid_t *uid);
+const struct got_error *gotd_parse_url(char **, char **, char **,
+ char **, const char *);
/* imsg.c */
const struct got_error *gotd_imsg_flush(struct imsgbuf *);
blob - /dev/null
blob + 59c09c35e0b26fb30a138fc1dd52d8f994826c32 (mode 644)
--- /dev/null
+++ gotd/libexec/Makefile
+SUBDIR = got-notify-email
+
+.include <bsd.subdir.mk>
blob - /dev/null
blob + 1bc739e7aa3fede7385d957470cee88dd2460099 (mode 644)
--- /dev/null
+++ gotd/libexec/Makefile.inc
+.include "../../Makefile.inc"
+
+realinstall:
+ ${INSTALL} ${INSTALL_COPY} -o ${BINOWN} -g ${BINGRP} \
+ -m ${BINMODE} ${PROG} ${LIBEXECDIR}/${PROG}
+
+NOMAN = Yes
blob - /dev/null
blob + bc6f702e1ea25327f3545179d493446d81da8948 (mode 644)
--- /dev/null
+++ gotd/libexec/got-notify-email/Makefile
+.PATH:${.CURDIR}/../..
+.PATH:${.CURDIR}/../../../lib
+
+.include "../../../got-version.mk"
+
+PROG= got-notify-email
+SRCS= got-notify-email.c pollfd.c error.c hash.c
+
+
+CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib
+
+.if defined(PROFILE)
+LDADD = -lutil_p -lz_p -lm_p
+.else
+LDADD = -lutil -lz -lm
+.endif
+
+DPADD = ${LIBZ} ${LIBUTIL}
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + 44ccd53effeef92d860796c2bea93b56f5aa539a (mode 644)
--- /dev/null
+++ gotd/libexec/got-notify-email/got-notify-email.c
+/*
+ * Copyright (c) 2024 Stefan Sperling <stsp@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <getopt.h>
+#include <err.h>
+#include <pwd.h>
+#include <netdb.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "got_error.h"
+
+#include "got_lib_poll.h"
+
+int smtp_timeout = 60; /* in seconds */
+
+__dead static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-f sender ] [-r responder] "
+ "[-s subject] [-h hostname] [-p port] recipient\n", getprogname());
+ exit(1);
+}
+
+static char *
+set_default_fromaddr(void)
+{
+ struct passwd *pw = NULL;
+ char *s;
+ char hostname[255];
+
+ pw = getpwuid(getuid());
+ if (pw == NULL) {
+ errx(1, "my UID %d was not found in password database",
+ getuid());
+ }
+
+ if (gethostname(hostname, sizeof(hostname)) == -1)
+ err(1, "gethostname");
+
+ if (asprintf(&s, "%s@%s", pw->pw_name, hostname) == -1)
+ err(1, "asprintf");
+
+ return s;
+}
+
+static int
+read_smtp_code(int s, const char *code)
+{
+ const struct got_error *error;
+ char buf[4];
+ size_t n;
+
+ error = got_poll_read_full_timeout(s, &n, buf, 3, 3, smtp_timeout);
+ if (error)
+ errx(1, "read: %s", error->msg);
+ if (strncmp(buf, code, 3) != 0) {
+ buf[3] = '\0';
+ warnx("unexpected SMTP message code: %s", buf);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+skip_to_crlf(int s)
+{
+ const struct got_error *error;
+ char buf[1];
+ size_t len;
+
+ for (;;) {
+ error = got_poll_read_full_timeout(s, &len, buf, 1, 1,
+ smtp_timeout);
+ if (error)
+ errx(1, "read: %s", error->msg);
+ if (buf[0] == '\r') {
+ error = got_poll_read_full(s, &len, buf, 1, 1);
+ if (error)
+ errx(1, "read: %s", error->msg);
+ if (buf[0] == '\n')
+ return 0;
+ }
+ }
+
+ return -1;
+}
+
+static int
+send_smtp_msg(int s, const char *fmt, ...)
+{
+ const struct got_error *error;
+ char buf[512];
+ int len;
+ va_list ap;
+
+ va_start(ap, fmt);
+ len = vsnprintf(buf, sizeof(buf), fmt, ap);
+ va_end(ap);
+ if (len < 0) {
+ warn("vsnprintf");
+ return -1;
+ }
+ if (len >= sizeof(buf)) {
+ warnx("%s: buffer too small for message '%s...'",
+ __func__, buf);
+ return -1;
+ }
+
+ error = got_poll_write_full(s, buf, len);
+ if (error) {
+ warnx("write: %s", error->msg);
+ return -1;
+ }
+
+ return 0;
+}
+
+static char *
+get_datestr(time_t *time, char *datebuf)
+{
+ struct tm mytm, *tm;
+ char *p, *s;
+
+ tm = gmtime_r(time, &mytm);
+ if (tm == NULL)
+ return NULL;
+ s = asctime_r(tm, datebuf);
+ if (s == NULL)
+ return NULL;
+ p = strchr(s, '\n');
+ if (p)
+ *p = '\0';
+ return s;
+}
+
+static void
+send_email(const char *myfromaddr, const char *fromaddr,
+ const char *recipient, const char *replytoaddr,
+ const char *subject, const char *hostname, const char *port)
+{
+ const struct got_error *error;
+ char *line = NULL;
+ size_t linesize = 0;
+ ssize_t linelen;
+ struct addrinfo hints, *res = NULL;
+ int s = -1, ret;
+ time_t now;
+ char datebuf[26];
+ char *datestr;
+
+ now = time(NULL);
+ datestr = get_datestr(&now, datebuf);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_INET;
+ hints.ai_socktype = SOCK_STREAM;
+
+ ret = getaddrinfo(hostname, port, &hints, &res);
+ if (ret)
+ errx(1, "getaddrinfo: %s", gai_strerror(ret));
+
+ s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+ if (s == -1)
+ err(1, "socket");
+
+ if (connect(s, res->ai_addr, res->ai_addrlen) == -1)
+ err(1, "connect %s:%s", hostname, port);
+
+ if (read_smtp_code(s, "220"))
+ errx(1, "unexpected SMTP greeting received");
+ if (skip_to_crlf(s))
+ errx(1, "invalid SMTP message received");
+
+ if (send_smtp_msg(s, "HELO localhost\r\n"))
+ errx(1, "could not send HELO");
+ if (read_smtp_code(s, "250"))
+ errx(1, "unexpected SMTP response received");
+ if (skip_to_crlf(s))
+ errx(1, "invalid SMTP message received");
+
+ if (send_smtp_msg(s, "MAIL FROM:<%s>\r\n", myfromaddr))
+ errx(1, "could not send MAIL FROM");
+ if (read_smtp_code(s, "250"))
+ errx(1, "unexpected SMTP response received");
+ if (skip_to_crlf(s))
+ errx(1, "invalid SMTP message received");
+
+ if (send_smtp_msg(s, "RCPT TO:<%s>\r\n", recipient))
+ errx(1, "could not send MAIL FROM");
+ if (read_smtp_code(s, "250"))
+ errx(1, "unexpected SMTP response received");
+ if (skip_to_crlf(s))
+ errx(1, "invalid SMTP message received");
+
+ if (send_smtp_msg(s, "DATA\r\n"))
+ errx(1, "could not send MAIL FROM");
+ if (read_smtp_code(s, "354"))
+ errx(1, "unexpected SMTP response received");
+ if (skip_to_crlf(s))
+ errx(1, "invalid SMTP message received");
+
+ if (send_smtp_msg(s, "From: %s\r\n", fromaddr))
+ errx(1, "could not send From header");
+ if (send_smtp_msg(s, "To: %s\r\n", recipient))
+ errx(1, "could not send To header");
+ if (replytoaddr) {
+ if (send_smtp_msg(s, "Reply-To: %s\r\n", replytoaddr))
+ errx(1, "could not send Reply-To header");
+ }
+ if (send_smtp_msg(s, "Date: %s +0000 (UTC)\r\n", datestr))
+ errx(1, "could not send Date header");
+
+ if (send_smtp_msg(s, "Subject: %s\r\n", subject))
+ errx(1, "could not send Subject header");
+
+ if (send_smtp_msg(s, "\r\n"))
+ errx(1, "could not send body delimiter");
+
+ while ((linelen = getline(&line, &linesize, stdin)) != -1) {
+ if (line[0] == '.') { /* dot stuffing */
+ error = got_poll_write_full(s, ".", 1);
+ if (error)
+ errx(1, "write: %s", error->msg);
+ }
+ error = got_poll_write_full(s, line, linelen);
+ if (error)
+ errx(1, "write: %s", error->msg);
+ }
+
+ if (send_smtp_msg(s, "\r\n.\r\n"))
+ errx(1, "could not send data terminator");
+ if (read_smtp_code(s, "250"))
+ errx(1, "unexpected SMTP response received");
+ if (skip_to_crlf(s))
+ errx(1, "invalid SMTP message received");
+
+ if (send_smtp_msg(s, "QUIT\r\n"))
+ errx(1, "could not send QUIT");
+
+ if (read_smtp_code(s, "221"))
+ errx(1, "unexpected SMTP response received");
+ if (skip_to_crlf(s))
+ errx(1, "invalid SMTP message received");
+
+ close(s);
+ free(line);
+ if (res)
+ freeaddrinfo(res);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *default_fromaddr = NULL;
+ const char *fromaddr = NULL, *recipient = NULL, *replytoaddr = NULL;
+ const char *subject = "gotd notification";
+ const char *hostname = "127.0.0.1";
+ const char *port = "25";
+ const char *errstr;
+ char *timeoutstr;
+ int ch;
+
+ while ((ch = getopt(argc, argv, "f:r:s:h:p:")) != -1) {
+ switch (ch) {
+ case 'h':
+ hostname = optarg;
+ break;
+ case 'f':
+ fromaddr = optarg;
+ break;
+ case 'p':
+ port = optarg;
+ break;
+ case 'r':
+ replytoaddr = optarg;
+ break;
+ case 's':
+ subject = optarg;
+ break;
+ default:
+ usage();
+ /* NOTREACHED */
+ break;
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc != 1)
+ usage();
+
+ /* used by the regression test suite */
+ timeoutstr = getenv("GOT_NOTIFY_EMAIL_TIMEOUT");
+ if (timeoutstr) {
+ smtp_timeout = strtonum(timeoutstr, 0, 600, &errstr);
+ if (errstr != NULL)
+ errx(1, "timeout in seconds is %s: %s",
+ errstr, timeoutstr);
+ }
+
+#ifndef PROFILE
+ if (pledge("stdio dns inet getpw", NULL) == -1)
+ err(1, "pledge");
+#endif
+ default_fromaddr = set_default_fromaddr();
+
+#ifndef PROFILE
+ if (pledge("stdio dns inet", NULL) == -1)
+ err(1, "pledge");
+#endif
+
+ recipient = argv[0];
+ if (fromaddr == NULL)
+ fromaddr = default_fromaddr;
+
+ send_email(default_fromaddr, fromaddr, recipient, replytoaddr,
+ subject, hostname, port);
+
+ free(default_fromaddr);
+ return 0;
+}
blob - 7f0792c2d67f76688341bc2d4b073d5d09180b4f
blob + 2d96ebe2bd1575fbb197e903423ccfec7e848614
--- gotd/parse.y
+++ gotd/parse.y
int lgetc(int);
int lungetc(int);
int findeol(void);
+static char *port_sprintf(int);
TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead);
struct sym {
struct gotd_repo *, char *);
static int conf_protect_branch(struct gotd_repo *,
char *);
+static int conf_notify_branch(struct gotd_repo *,
+ char *);
+static int conf_notify_ref_namespace(struct gotd_repo *,
+ char *);
+static int conf_notify_email(struct gotd_repo *,
+ char *, char *, char *, char *, char *);
+static int conf_notify_http(struct gotd_repo *,
+ char *, char *, char *);
static enum gotd_procid gotd_proc_id;
typedef struct {
%token PATH ERROR LISTEN ON USER REPOSITORY PERMIT DENY
%token RO RW CONNECTION LIMIT REQUEST TIMEOUT
-%token PROTECT NAMESPACE BRANCH TAG
+%token PROTECT NAMESPACE BRANCH TAG REFERENCE RELAY PORT
+%token NOTIFY EMAIL FROM REPLY TO URL PASSWORD
%token <v.string> STRING
%token <v.number> NUMBER
}
;
+notify : NOTIFY '{' optnl notifyflags_l '}'
+ | NOTIFY notifyflags
+
+notifyflags_l : notifyflags optnl notifyflags_l
+ | notifyflags optnl
+ ;
+
+notifyflags : BRANCH STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_branch(new_repo, $2)) {
+ free($2);
+ YYERROR;
+ }
+ free($2);
+ }
+ }
+ | REFERENCE NAMESPACE STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_ref_namespace(new_repo, $3)) {
+ free($3);
+ YYERROR;
+ }
+ free($3);
+ }
+ }
+ | EMAIL TO STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, NULL, $3,
+ NULL, NULL, NULL)) {
+ free($3);
+ YYERROR;
+ }
+ free($3);
+ }
+ }
+ | EMAIL FROM STRING TO STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, $3, $5,
+ NULL, NULL, NULL)) {
+ free($3);
+ free($5);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ }
+ }
+ | EMAIL TO STRING REPLY TO STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, NULL, $3,
+ $6, NULL, NULL)) {
+ free($3);
+ free($6);
+ YYERROR;
+ }
+ free($3);
+ free($6);
+ }
+ }
+ | EMAIL FROM STRING TO STRING REPLY TO STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, $3, $5,
+ $8, NULL, NULL)) {
+ free($3);
+ free($5);
+ free($8);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ free($8);
+ }
+ }
+ | EMAIL TO STRING RELAY STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, NULL, $3,
+ NULL, $5, NULL)) {
+ free($3);
+ free($5);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ }
+ }
+ | EMAIL FROM STRING TO STRING RELAY STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, $3, $5,
+ NULL, $7, NULL)) {
+ free($3);
+ free($5);
+ free($7);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ free($7);
+ }
+ }
+ | EMAIL TO STRING REPLY TO STRING RELAY STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, NULL, $3,
+ $6, $8, NULL)) {
+ free($3);
+ free($6);
+ free($8);
+ YYERROR;
+ }
+ free($3);
+ free($6);
+ free($8);
+ }
+ }
+ | EMAIL FROM STRING TO STRING REPLY TO STRING RELAY STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, $3, $5,
+ $8, $10, NULL)) {
+ free($3);
+ free($5);
+ free($8);
+ free($10);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ free($8);
+ free($10);
+ }
+ }
+ | EMAIL TO STRING RELAY STRING PORT STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, NULL, $3,
+ NULL, $5, $7)) {
+ free($3);
+ free($5);
+ free($7);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ free($7);
+ }
+ }
+ | EMAIL FROM STRING TO STRING RELAY STRING PORT STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, $3, $5,
+ NULL, $7, $9)) {
+ free($3);
+ free($5);
+ free($7);
+ free($9);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ free($7);
+ free($9);
+ }
+ }
+ | EMAIL TO STRING REPLY TO STRING RELAY STRING PORT STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, NULL, $3,
+ $6, $8, $10)) {
+ free($3);
+ free($6);
+ free($8);
+ free($10);
+ YYERROR;
+ }
+ free($3);
+ free($6);
+ free($8);
+ free($10);
+ }
+ }
+ | EMAIL FROM STRING TO STRING REPLY TO STRING RELAY STRING PORT STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, $3, $5,
+ $8, $10, $12)) {
+ free($3);
+ free($5);
+ free($8);
+ free($10);
+ free($12);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ free($8);
+ free($10);
+ free($12);
+ }
+ }
+ | EMAIL TO STRING RELAY STRING PORT NUMBER {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, NULL, $3,
+ NULL, $5, port_sprintf($7))) {
+ free($3);
+ free($5);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ }
+ }
+ | EMAIL FROM STRING TO STRING RELAY STRING PORT NUMBER {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, $3, $5,
+ NULL, $7, port_sprintf($9))) {
+ free($3);
+ free($5);
+ free($7);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ free($7);
+ }
+ }
+ | EMAIL TO STRING REPLY TO STRING RELAY STRING PORT NUMBER {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, NULL, $3,
+ $6, $8, port_sprintf($10))) {
+ free($3);
+ free($6);
+ free($8);
+ YYERROR;
+ }
+ free($3);
+ free($6);
+ free($8);
+ }
+ }
+ | EMAIL FROM STRING TO STRING REPLY TO STRING RELAY STRING PORT NUMBER {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_email(new_repo, $3, $5,
+ $8, $10, port_sprintf($12))) {
+ free($3);
+ free($5);
+ free($8);
+ free($10);
+ YYERROR;
+ }
+ free($3);
+ free($5);
+ free($8);
+ free($10);
+ }
+ }
+ | URL STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_http(new_repo, $2, NULL,
+ NULL)) {
+ free($2);
+ YYERROR;
+ }
+ free($2);
+ }
+ }
+ | URL STRING USER STRING PASSWORD STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_http(new_repo, $2, $4, $6)) {
+ free($2);
+ free($4);
+ free($6);
+ YYERROR;
+ }
+ free($2);
+ free($4);
+ free($6);
+ }
+ }
+ ;
+
repository : REPOSITORY STRING {
struct gotd_repo *repo;
if (gotd_proc_id == PROC_GOTD ||
gotd_proc_id == PROC_AUTH ||
gotd_proc_id == PROC_REPO_WRITE ||
- gotd_proc_id == PROC_GITWRAPPER) {
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_GITWRAPPER |
+ gotd_proc_id == PROC_NOTIFY) {
new_repo = conf_new_repo($2);
}
free($2);
if (gotd_proc_id == PROC_GOTD ||
gotd_proc_id == PROC_AUTH ||
gotd_proc_id == PROC_REPO_WRITE ||
- gotd_proc_id == PROC_GITWRAPPER) {
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_GITWRAPPER ||
+ gotd_proc_id == PROC_NOTIFY) {
if (!got_path_is_absolute($2)) {
yyerror("%s: path %s is not absolute",
__func__, $2);
free($2);
}
| protect
+ | notify
;
repoopts2 : repoopts2 repoopts1 nl
{ "branch", BRANCH },
{ "connection", CONNECTION },
{ "deny", DENY },
+ { "email", EMAIL },
+ { "from", FROM },
{ "limit", LIMIT },
{ "listen", LISTEN },
{ "namespace", NAMESPACE },
+ { "notify", NOTIFY },
{ "on", ON },
+ { "password", PASSWORD },
{ "path", PATH },
{ "permit", PERMIT },
+ { "port", PORT },
{ "protect", PROTECT },
+ { "reference", REFERENCE },
+ { "relay", RELAY },
+ { "reply", REPLY },
{ "repository", REPOSITORY },
{ "request", REQUEST },
{ "ro", RO },
{ "rw", RW },
{ "tag", TAG },
{ "timeout", TIMEOUT },
+ { "to", TO },
+ { "url", URL },
{ "user", USER },
};
const struct keywords *p;
TAILQ_INIT(&repo->protected_tag_namespaces);
TAILQ_INIT(&repo->protected_branch_namespaces);
TAILQ_INIT(&repo->protected_branches);
+ TAILQ_INIT(&repo->protected_branches);
+ TAILQ_INIT(&repo->notification_refs);
+ TAILQ_INIT(&repo->notification_ref_namespaces);
+ STAILQ_INIT(&repo->notification_targets);
if (strlcpy(repo->name, name, sizeof(repo->name)) >=
sizeof(repo->name))
yyerror("got_pathlist_insert: %s", error->msg);
else
yyerror("duplicate protect branch %s", branchname);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+conf_notify_branch(struct gotd_repo *repo, char *branchname)
+{
+ const struct got_error *error;
+ struct got_pathlist_entry *pe;
+ char *refname;
+
+ if (strncmp(branchname, "refs/heads/", 11) != 0) {
+ if (asprintf(&refname, "refs/heads/%s", branchname) == -1) {
+ yyerror("asprintf: %s", strerror(errno));
+ return -1;
+ }
+ } else {
+ refname = strdup(branchname);
+ if (refname == NULL) {
+ yyerror("strdup: %s", strerror(errno));
+ return -1;
+ }
+ }
+
+ if (!refname_is_valid(refname)) {
+ free(refname);
return -1;
}
+ error = got_pathlist_insert(&pe, &repo->notification_refs,
+ refname, NULL);
+ if (error) {
+ free(refname);
+ yyerror("got_pathlist_insert: %s", error->msg);
+ return -1;
+ }
+ if (pe == NULL)
+ free(refname);
+
return 0;
}
+static int
+conf_notify_ref_namespace(struct gotd_repo *repo, char *namespace)
+{
+ const struct got_error *error;
+ struct got_pathlist_entry *pe;
+ char *s;
+
+ got_path_strip_trailing_slashes(namespace);
+ if (!refname_is_valid(namespace))
+ return -1;
+
+ if (asprintf(&s, "%s/", namespace) == -1) {
+ yyerror("asprintf: %s", strerror(errno));
+ return -1;
+ }
+
+ error = got_pathlist_insert(&pe, &repo->notification_ref_namespaces,
+ s, NULL);
+ if (error) {
+ free(s);
+ yyerror("got_pathlist_insert: %s", error->msg);
+ return -1;
+ }
+ if (pe == NULL)
+ free(s);
+
+ return 0;
+}
+
+static int
+conf_notify_email(struct gotd_repo *repo, char *sender, char *recipient,
+ char *responder, char *hostname, char *port)
+{
+ struct gotd_notification_target *target;
+
+ STAILQ_FOREACH(target, &repo->notification_targets, entry) {
+ if (target->type != GOTD_NOTIFICATION_VIA_EMAIL)
+ continue;
+ if (strcmp(target->conf.email.recipient, recipient) == 0) {
+ yyerror("duplicate email notification for '%s' in "
+ "repository '%s'", recipient, repo->name);
+ return -1;
+ }
+ }
+
+ target = calloc(1, sizeof(*target));
+ if (target == NULL)
+ fatal("calloc");
+ target->type = GOTD_NOTIFICATION_VIA_EMAIL;
+ if (sender) {
+ target->conf.email.sender = strdup(sender);
+ if (target->conf.email.sender == NULL)
+ fatal("strdup");
+ }
+ target->conf.email.recipient = strdup(recipient);
+ if (target->conf.email.recipient == NULL)
+ fatal("strdup");
+ if (responder) {
+ target->conf.email.responder = strdup(responder);
+ if (target->conf.email.responder == NULL)
+ fatal("strdup");
+ }
+ if (hostname) {
+ target->conf.email.hostname = strdup(hostname);
+ if (target->conf.email.hostname == NULL)
+ fatal("strdup");
+ }
+ if (port) {
+ target->conf.email.port = strdup(port);
+ if (target->conf.email.port == NULL)
+ fatal("strdup");
+ }
+
+ STAILQ_INSERT_TAIL(&repo->notification_targets, target, entry);
+ return 0;
+}
+
+static int
+conf_notify_http(struct gotd_repo *repo, char *url, char *user, char *password)
+{
+ const struct got_error *error;
+ struct gotd_notification_target *target;
+ char *proto, *host, *port, *request_path;
+ int ret = 0;
+
+ error = gotd_parse_url(&proto, &host, &port, &request_path, url);
+ if (error) {
+ yyerror("invalid HTTP notification URL '%s' in "
+ "repository '%s': %s", url, repo->name, error->msg);
+ return -1;
+ }
+
+ if (strcmp(proto, "http") != 0 && strcmp(proto, "https") != 0) {
+ yyerror("invalid protocol '%s' in notification URL '%s' in "
+ "repository '%s", proto, url, repo->name);
+ ret = -1;
+ goto done;
+ }
+
+ if (strcmp(proto, "http") == 0 && (user != NULL || password != NULL)) {
+ log_warnx("%s: WARNING: Using basic authentication over "
+ "plaintext http:// will leak credentials; https:// is "
+ "recommended for URL '%s'", getprogname(), url);
+ }
+
+ STAILQ_FOREACH(target, &repo->notification_targets, entry) {
+ if (target->type != GOTD_NOTIFICATION_VIA_HTTP)
+ continue;
+ if (strcmp(target->conf.http.url, url) == 0) {
+ yyerror("duplicate notification for URL '%s' in "
+ "repository '%s'", url, repo->name);
+ ret = -1;
+ goto done;
+ }
+ }
+
+ target = calloc(1, sizeof(*target));
+ if (target == NULL)
+ fatal("calloc");
+ target->type = GOTD_NOTIFICATION_VIA_HTTP;
+ target->conf.http.url = strdup(url);
+ if (target->conf.http.url == NULL)
+ fatal("calloc");
+ if (user) {
+ target->conf.http.user = strdup(user);
+ if (target->conf.http.user == NULL)
+ fatal("calloc");
+ }
+ if (password) {
+ target->conf.http.password = strdup(password);
+ if (target->conf.http.password == NULL)
+ fatal("calloc");
+ }
+
+ STAILQ_INSERT_TAIL(&repo->notification_targets, target, entry);
+done:
+ free(proto);
+ free(host);
+ free(port);
+ free(request_path);
+ return ret;
+}
+
int
symset(const char *nam, const char *val, int persist)
{
}
struct gotd_repo *
-gotd_find_repo_by_name(const char *repo_name, struct gotd *gotd)
+gotd_find_repo_by_name(const char *repo_name, struct gotd_repolist *repos)
{
struct gotd_repo *repo;
size_t namelen;
- TAILQ_FOREACH(repo, &gotd->repos, entry) {
+ TAILQ_FOREACH(repo, repos, entry) {
namelen = strlen(repo->name);
if (strncmp(repo->name, repo_name, namelen) != 0)
continue;
return -1;
return 0;
}
+
+const struct got_error *
+gotd_parse_url(char **proto, char **host, char **port,
+ char **request_path, const char *url)
+{
+ const struct got_error *err = NULL;
+ char *s, *p, *q;
+
+ *proto = *host = *port = *request_path = NULL;
+
+ p = strstr(url, "://");
+ if (!p)
+ return got_error(GOT_ERR_PARSE_URI);
+
+ *proto = strndup(url, p - url);
+ if (*proto == NULL) {
+ err = got_error_from_errno("strndup");
+ goto done;
+ }
+ s = p + 3;
+
+ p = strstr(s, "/");
+ if (p == NULL || strlen(p) == 1) {
+ err = got_error(GOT_ERR_PARSE_URI);
+ goto done;
+ }
+
+ q = memchr(s, ':', p - s);
+ if (q) {
+ *host = strndup(s, q - s);
+ if (*host == NULL) {
+ err = got_error_from_errno("strndup");
+ goto done;
+ }
+ if ((*host)[0] == '\0') {
+ err = got_error(GOT_ERR_PARSE_URI);
+ goto done;
+ }
+ *port = strndup(q + 1, p - (q + 1));
+ if (*port == NULL) {
+ err = got_error_from_errno("strndup");
+ goto done;
+ }
+ if ((*port)[0] == '\0') {
+ err = got_error(GOT_ERR_PARSE_URI);
+ goto done;
+ }
+ } else {
+ *host = strndup(s, p - s);
+ if (*host == NULL) {
+ err = got_error_from_errno("strndup");
+ goto done;
+ }
+ if ((*host)[0] == '\0') {
+ err = got_error(GOT_ERR_PARSE_URI);
+ goto done;
+ }
+ }
+
+ while (p[0] == '/' && p[1] == '/')
+ p++;
+ *request_path = strdup(p);
+ if (*request_path == NULL) {
+ err = got_error_from_errno("strdup");
+ goto done;
+ }
+ got_path_strip_trailing_slashes(*request_path);
+ if ((*request_path)[0] == '\0') {
+ err = got_error(GOT_ERR_PARSE_URI);
+ goto done;
+ }
+done:
+ if (err) {
+ free(*proto);
+ *proto = NULL;
+ free(*host);
+ *host = NULL;
+ free(*port);
+ *port = NULL;
+ free(*request_path);
+ *request_path = NULL;
+ }
+ return err;
+}
+
+static char *
+port_sprintf(int p)
+{
+ static char portno[32];
+ int n;
+
+ n = snprintf(portno, sizeof(portno), "%lld", (long long)p);
+ if (n < 0 || (size_t)n >= sizeof(portno))
+ fatalx("port number too long: %lld", (long long)p);
+
+ return portno;
+}
blob - de2eb3a42ab209cd4a4e18b35625f81b1d88e57a
blob + 257518e5da203ab6fc6c9aace1cc717c714799ed
--- gotd/privsep_stub.c
+++ gotd/privsep_stub.c
{
return got_error(GOT_ERR_NOT_IMPL);
}
+
+const struct got_error *
+got_traverse_packed_commits(struct got_object_id_queue *traversed_commits,
+ struct got_object_id *commit_id, const char *path,
+ struct got_repository *repo)
+{
+ return NULL;
+}
blob - /dev/null
blob + 8b7babf0b8bfda73dde136e6486c27085a6c14f4 (mode 644)
--- /dev/null
+++ gotd/notify.c
+/*
+ * Copyright (c) 2024 Stefan Sperling <stsp@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/tree.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <event.h>
+#include <siphash.h>
+#include <limits.h>
+#include <sha1.h>
+#include <sha2.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <imsg.h>
+#include <unistd.h>
+
+#include "got_error.h"
+#include "got_path.h"
+
+#include "gotd.h"
+#include "log.h"
+#include "notify.h"
+
+#ifndef nitems
+#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+static struct gotd_notify {
+ pid_t pid;
+ const char *title;
+ struct gotd_imsgev parent_iev;
+ struct gotd_repolist *repos;
+ const char *default_sender;
+} gotd_notify;
+
+struct gotd_notify_session {
+ STAILQ_ENTRY(gotd_notify_session) entry;
+ uint32_t id;
+ struct gotd_imsgev iev;
+};
+STAILQ_HEAD(gotd_notify_sessions, gotd_notify_session);
+
+static struct gotd_notify_sessions gotd_notify_sessions[GOTD_CLIENT_TABLE_SIZE];
+static SIPHASH_KEY sessions_hash_key;
+
+static void gotd_notify_shutdown(void);
+
+static uint64_t
+session_hash(uint32_t session_id)
+{
+ return SipHash24(&sessions_hash_key, &session_id, sizeof(session_id));
+}
+
+static void
+add_session(struct gotd_notify_session *session)
+{
+ uint64_t slot;
+
+ slot = session_hash(session->id) % nitems(gotd_notify_sessions);
+ STAILQ_INSERT_HEAD(&gotd_notify_sessions[slot], session, entry);
+}
+
+static struct gotd_notify_session *
+find_session(uint32_t session_id)
+{
+ uint64_t slot;
+ struct gotd_notify_session *s;
+
+ slot = session_hash(session_id) % nitems(gotd_notify_sessions);
+ STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
+ if (s->id == session_id)
+ return s;
+ }
+
+ return NULL;
+}
+
+static struct gotd_notify_session *
+find_session_by_fd(int fd)
+{
+ uint64_t slot;
+ struct gotd_notify_session *s;
+
+ for (slot = 0; slot < nitems(gotd_notify_sessions); slot++) {
+ STAILQ_FOREACH(s, &gotd_notify_sessions[slot], entry) {
+ if (s->iev.ibuf.fd == fd)
+ return s;
+ }
+
+ }
+
+ return NULL;
+}
+
+
+static void
+remove_session(struct gotd_notify_session *session)
+{
+ uint64_t slot;
+
+ slot = session_hash(session->id) % nitems(gotd_notify_sessions);
+ STAILQ_REMOVE(&gotd_notify_sessions[slot], session,
+ gotd_notify_session, entry);
+ free(session);
+}
+
+static uint32_t
+get_session_id(void)
+{
+ int duplicate = 0;
+ uint32_t id;
+
+ do {
+ id = arc4random();
+ duplicate = (find_session(id) != NULL);
+ } while (duplicate || id == 0);
+
+ return id;
+}
+
+static void
+gotd_notify_sighdlr(int sig, short event, void *arg)
+{
+ /*
+ * Normal signal handler rules don't apply because libevent
+ * decouples for us.
+ */
+
+ switch (sig) {
+ case SIGHUP:
+ log_info("%s: ignoring SIGHUP", __func__);
+ break;
+ case SIGUSR1:
+ log_info("%s: ignoring SIGUSR1", __func__);
+ break;
+ case SIGTERM:
+ case SIGINT:
+ gotd_notify_shutdown();
+ /* NOTREACHED */
+ break;
+ default:
+ fatalx("unexpected signal");
+ }
+}
+
+static void
+run_notification_helper(const char *prog, const char **argv, int fd)
+{
+ const struct got_error *err = NULL;
+ pid_t pid;
+ int child_status;
+
+ pid = fork();
+ if (pid == -1) {
+ err = got_error_from_errno("fork");
+ log_warn("%s", err->msg);
+ return;
+ } else if (pid == 0) {
+ signal(SIGQUIT, SIG_DFL);
+ signal(SIGINT, SIG_DFL);
+ signal(SIGCHLD, SIG_DFL);
+
+ if (dup2(fd, STDIN_FILENO) == -1) {
+ fprintf(stderr, "%s: dup2: %s\n", getprogname(),
+ strerror(errno));
+ _exit(1);
+ }
+
+ closefrom(STDERR_FILENO + 1);
+
+ if (execv(prog, (char *const *)argv) == -1) {
+ fprintf(stderr, "%s: exec %s: %s\n", getprogname(),
+ prog, strerror(errno));
+ _exit(1);
+ }
+
+ /* not reached */
+ }
+
+ if (waitpid(pid, &child_status, 0) == -1) {
+ err = got_error_from_errno("waitpid");
+ goto done;
+ }
+
+ if (!WIFEXITED(child_status)) {
+ err = got_error(GOT_ERR_PRIVSEP_DIED);
+ goto done;
+ }
+
+ if (WEXITSTATUS(child_status) != 0)
+ err = got_error(GOT_ERR_PRIVSEP_EXIT);
+done:
+ if (err)
+ log_warnx("%s: child %s pid %d: %s", gotd_notify.title,
+ prog, pid, err->msg);
+}
+
+static void
+notify_email(struct gotd_notification_target *target, const char *subject_line,
+ int fd)
+{
+ const char *argv[13];
+ int i = 0;
+
+ argv[i++] = GOTD_PATH_PROG_NOTIFY_EMAIL;
+
+ argv[i++] = "-f";
+ if (target->conf.email.sender)
+ argv[i++] = target->conf.email.sender;
+ else
+ argv[i++] = gotd_notify.default_sender;
+
+ if (target->conf.email.responder) {
+ argv[i++] = "-r";
+ argv[i++] = target->conf.email.responder;
+ }
+
+ if (target->conf.email.hostname) {
+ argv[i++] = "-h";
+ argv[i++] = target->conf.email.hostname;
+ }
+
+ if (target->conf.email.port) {
+ argv[i++] = "-p";
+ argv[i++] = target->conf.email.port;
+ }
+
+ argv[i++] = "-s";
+ argv[i++] = subject_line;
+
+ argv[i++] = target->conf.email.recipient;
+
+ argv[i] = NULL;
+
+ run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL, argv, fd);
+}
+
+static void
+notify_http(struct gotd_notification_target *target, const char *subject_line,
+ int fd)
+{
+ const char *argv[10] = { 0 }; /* TODO */
+
+ run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd);
+}
+
+static const struct got_error *
+send_notification(struct imsg *imsg, struct gotd_imsgev *iev)
+{
+ const struct got_error *err = NULL;
+ struct gotd_imsg_notify inotify;
+ size_t datalen;
+ struct gotd_repo *repo;
+ struct gotd_notification_target *target;
+ int fd;
+
+ datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+ if (datalen != sizeof(inotify))
+ return got_error(GOT_ERR_PRIVSEP_LEN);
+
+ memcpy(&inotify, imsg->data, datalen);
+
+ repo = gotd_find_repo_by_name(inotify.repo_name, gotd_notify.repos);
+ if (repo == NULL)
+ return got_error(GOT_ERR_PRIVSEP_MSG);
+
+ fd = imsg_get_fd(imsg);
+ if (fd == -1)
+ return got_error(GOT_ERR_PRIVSEP_NO_FD);
+
+ if (lseek(fd, 0, SEEK_SET) == -1) {
+ err = got_error_from_errno("lseek");
+ goto done;
+ }
+
+ STAILQ_FOREACH(target, &repo->notification_targets, entry) {
+ switch (target->type) {
+ case GOTD_NOTIFICATION_VIA_EMAIL:
+ notify_email(target, inotify.subject_line, fd);
+ break;
+ case GOTD_NOTIFICATION_VIA_HTTP:
+ notify_http(target, inotify.subject_line, fd);
+ break;
+ }
+ }
+
+ if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFICATION_SENT,
+ PROC_NOTIFY, -1, NULL, 0) == -1) {
+ err = got_error_from_errno("imsg compose NOTIFY");
+ goto done;
+ }
+done:
+ close(fd);
+ return err;
+}
+
+static void
+notify_dispatch_session(int fd, short event, void *arg)
+{
+ struct gotd_imsgev *iev = arg;
+ struct imsgbuf *ibuf = &iev->ibuf;
+ ssize_t n;
+ int shut = 0;
+ struct imsg imsg;
+
+ if (event & EV_READ) {
+ if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+ fatal("imsg_read error");
+ if (n == 0) {
+ /* Connection closed. */
+ shut = 1;
+ goto done;
+ }
+ }
+
+ if (event & EV_WRITE) {
+ n = msgbuf_write(&ibuf->w);
+ if (n == -1 && errno != EAGAIN)
+ fatal("msgbuf_write");
+ if (n == 0) {
+ /* Connection closed. */
+ shut = 1;
+ goto done;
+ }
+ }
+
+ for (;;) {
+ const struct got_error *err = NULL;
+
+ if ((n = imsg_get(ibuf, &imsg)) == -1)
+ fatal("%s: imsg_get error", __func__);
+ if (n == 0) /* No more messages. */
+ break;
+
+ switch (imsg.hdr.type) {
+ case GOTD_IMSG_NOTIFY:
+ err = send_notification(&imsg, iev);
+ break;
+ default:
+ log_debug("unexpected imsg %d", imsg.hdr.type);
+ break;
+ }
+ imsg_free(&imsg);
+
+ if (err)
+ log_warnx("%s: %s", __func__, err->msg);
+ }
+done:
+ if (!shut) {
+ gotd_imsg_event_add(iev);
+ } else {
+ struct gotd_notify_session *session;
+
+ /* This pipe is dead. Remove its event handler */
+ event_del(&iev->ev);
+ imsg_clear(&iev->ibuf);
+
+ session = find_session_by_fd(fd);
+ if (session)
+ remove_session(session);
+ }
+}
+
+static const struct got_error *
+recv_session(struct imsg *imsg)
+{
+ struct gotd_notify_session *session;
+ size_t datalen;
+ int fd;
+
+ datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+ if (datalen != 0)
+ return got_error(GOT_ERR_PRIVSEP_LEN);
+
+ fd = imsg_get_fd(imsg);
+ if (fd == -1)
+ return got_error(GOT_ERR_PRIVSEP_NO_FD);
+
+ session = calloc(1, sizeof(*session));
+ if (session == NULL)
+ return got_error_from_errno("calloc");
+
+ session->id = get_session_id();
+ imsg_init(&session->iev.ibuf, fd);
+ session->iev.handler = notify_dispatch_session;
+ session->iev.events = EV_READ;
+ session->iev.handler_arg = NULL;
+ event_set(&session->iev.ev, session->iev.ibuf.fd, EV_READ,
+ notify_dispatch_session, &session->iev);
+ gotd_imsg_event_add(&session->iev);
+ add_session(session);
+
+ return NULL;
+}
+
+static void
+notify_dispatch(int fd, short event, void *arg)
+{
+ struct gotd_imsgev *iev = arg;
+ struct imsgbuf *ibuf = &iev->ibuf;
+ ssize_t n;
+ int shut = 0;
+ struct imsg imsg;
+
+ if (event & EV_READ) {
+ if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+ fatal("imsg_read error");
+ if (n == 0) {
+ /* Connection closed. */
+ shut = 1;
+ goto done;
+ }
+ }
+
+ if (event & EV_WRITE) {
+ n = msgbuf_write(&ibuf->w);
+ if (n == -1 && errno != EAGAIN)
+ fatal("msgbuf_write");
+ if (n == 0) {
+ /* Connection closed. */
+ shut = 1;
+ goto done;
+ }
+ }
+
+ for (;;) {
+ const struct got_error *err = NULL;
+
+ if ((n = imsg_get(ibuf, &imsg)) == -1)
+ fatal("%s: imsg_get error", __func__);
+ if (n == 0) /* No more messages. */
+ break;
+
+ switch (imsg.hdr.type) {
+ case GOTD_IMSG_CONNECT_SESSION:
+ err = recv_session(&imsg);
+ break;
+ default:
+ log_debug("unexpected imsg %d", imsg.hdr.type);
+ break;
+ }
+ imsg_free(&imsg);
+
+ if (err)
+ log_warnx("%s: %s", __func__, err->msg);
+ }
+done:
+ if (!shut) {
+ gotd_imsg_event_add(iev);
+ } else {
+ /* This pipe is dead. Remove its event handler */
+ event_del(&iev->ev);
+ event_loopexit(NULL);
+ }
+
+}
+
+void
+notify_main(const char *title, struct gotd_repolist *repos,
+ const char *default_sender)
+{
+ const struct got_error *err = NULL;
+ struct event evsigint, evsigterm, evsighup, evsigusr1;
+
+ arc4random_buf(&sessions_hash_key, sizeof(sessions_hash_key));
+
+ gotd_notify.title = title;
+ gotd_notify.repos = repos;
+ gotd_notify.default_sender = default_sender;
+ gotd_notify.pid = getpid();
+
+ signal_set(&evsigint, SIGINT, gotd_notify_sighdlr, NULL);
+ signal_set(&evsigterm, SIGTERM, gotd_notify_sighdlr, NULL);
+ signal_set(&evsighup, SIGHUP, gotd_notify_sighdlr, NULL);
+ signal_set(&evsigusr1, SIGUSR1, gotd_notify_sighdlr, NULL);
+ signal(SIGPIPE, SIG_IGN);
+
+ signal_add(&evsigint, NULL);
+ signal_add(&evsigterm, NULL);
+ signal_add(&evsighup, NULL);
+ signal_add(&evsigusr1, NULL);
+
+ imsg_init(&gotd_notify.parent_iev.ibuf, GOTD_FILENO_MSG_PIPE);
+ gotd_notify.parent_iev.handler = notify_dispatch;
+ gotd_notify.parent_iev.events = EV_READ;
+ gotd_notify.parent_iev.handler_arg = NULL;
+ event_set(&gotd_notify.parent_iev.ev, gotd_notify.parent_iev.ibuf.fd,
+ EV_READ, notify_dispatch, &gotd_notify.parent_iev);
+ gotd_imsg_event_add(&gotd_notify.parent_iev);
+
+ event_dispatch();
+
+ if (err)
+ log_warnx("%s: %s", title, err->msg);
+ gotd_notify_shutdown();
+}
+
+void
+gotd_notify_shutdown(void)
+{
+ log_debug("shutting down");
+ exit(0);
+}
blob - /dev/null
blob + 8173549e2a96f7a2443ff8bd53806411e4312d57 (mode 644)
--- /dev/null
+++ gotd/notify.h
+/*
+ * Copyright (c) 2024 Stefan Sperling <stsp@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+void notify_main(const char *, struct gotd_repolist *, const char *);
blob - 27ecb3cb1f2dfa87ae9b75649146a015b1e45941
blob + 7e12b3db952dc5257bc13548172003e037a91305
--- gotd/repo_write.c
+++ gotd/repo_write.c
#include <sys/stat.h>
#include <sys/types.h>
+#include <ctype.h>
#include <event.h>
#include <errno.h>
#include <imsg.h>
#include "got_object.h"
#include "got_reference.h"
#include "got_path.h"
+#include "got_diff.h"
+#include "got_cancel.h"
+#include "got_commit_graph.h"
+#include "got_opentemp.h"
#include "got_lib_delta.h"
#include "got_lib_delta_cache.h"
struct got_pathlist_head *protected_tag_namespaces;
struct got_pathlist_head *protected_branch_namespaces;
struct got_pathlist_head *protected_branches;
+ struct {
+ FILE *f1;
+ FILE *f2;
+ int fd1;
+ int fd2;
+ } diff;
} repo_write;
struct gotd_ref_update {
return got_error(GOT_ERR_PRIVSEP_NO_FD);
return NULL;
+}
+
+static char *
+get_datestr(time_t *time, char *datebuf)
+{
+ struct tm mytm, *tm;
+ char *p, *s;
+
+ tm = gmtime_r(time, &mytm);
+ if (tm == NULL)
+ return NULL;
+ s = asctime_r(tm, datebuf);
+ if (s == NULL)
+ return NULL;
+ p = strchr(s, '\n');
+ if (p)
+ *p = '\0';
+ return s;
+}
+
+static const struct got_error *
+notify_removed_ref(const char *refname, uint8_t *sha1,
+ struct gotd_imsgev *iev, int fd)
+{
+ const struct got_error *err;
+ struct got_object_id id;
+ char *id_str;
+
+ memset(&id, 0, sizeof(id));
+ memcpy(id.sha1, sha1, sizeof(id.sha1));
+
+ err = got_object_id_str(&id_str, &id);
+ if (err)
+ return err;
+
+ dprintf(fd, "Removed %s: %s\n", refname, id_str);
+ free(id_str);
+ return err;
}
+static const char *
+format_author(char *author)
+{
+ char *smallerthan;
+
+ smallerthan = strchr(author, '<');
+ if (smallerthan && smallerthan[1] != '\0')
+ author = smallerthan + 1;
+ author[strcspn(author, "@>")] = '\0';
+
+ return author;
+}
+
+static const struct got_error *
+print_commit_oneline(struct got_commit_object *commit, struct got_object_id *id,
+ struct got_repository *repo, int fd)
+{
+ const struct got_error *err = NULL;
+ char *id_str = NULL, *logmsg0 = NULL;
+ char *s, *nl;
+ char *committer = NULL, *author = NULL;
+ char datebuf[12]; /* YYYY-MM-DD + SPACE + NUL */
+ struct tm tm;
+ time_t committer_time;
+
+ err = got_object_id_str(&id_str, id);
+ if (err)
+ return err;
+
+ committer_time = got_object_commit_get_committer_time(commit);
+ if (gmtime_r(&committer_time, &tm) == NULL) {
+ err = got_error_from_errno("gmtime_r");
+ goto done;
+ }
+ if (strftime(datebuf, sizeof(datebuf), "%G-%m-%d ", &tm) == 0) {
+ err = got_error(GOT_ERR_NO_SPACE);
+ goto done;
+ }
+
+ err = got_object_commit_get_logmsg(&logmsg0, commit);
+ if (err)
+ goto done;
+
+ s = logmsg0;
+ while (isspace((unsigned char)s[0]))
+ s++;
+
+ nl = strchr(s, '\n');
+ if (nl) {
+ *nl = '\0';
+ }
+
+ if (strcmp(got_object_commit_get_author(commit),
+ got_object_commit_get_committer(commit)) != 0) {
+ author = strdup(got_object_commit_get_author(commit));
+ if (author == NULL) {
+ err = got_error_from_errno("strdup");
+ goto done;
+ }
+ dprintf(fd, "%s%.7s %.8s %s\n", datebuf, id_str,
+ format_author(author), s);
+ } else {
+ committer = strdup(got_object_commit_get_committer(commit));
+ if (committer == NULL) {
+ err = got_error_from_errno("strdup");
+ goto done;
+ }
+ dprintf(fd, "%s%.7s %.8s %s\n", datebuf, id_str,
+ format_author(committer), s);
+ }
+
+ if (fsync(fd) == -1 && err == NULL)
+ err = got_error_from_errno("fsync");
+done:
+ free(id_str);
+ free(logmsg0);
+ free(committer);
+ free(author);
+ return err;
+}
+
+static const struct got_error *
+print_diffstat(struct got_diffstat_cb_arg *dsa, int fd)
+{
+ struct got_pathlist_entry *pe;
+
+ TAILQ_FOREACH(pe, dsa->paths, entry) {
+ struct got_diff_changed_path *cp = pe->data;
+ int pad = dsa->max_path_len - pe->path_len + 1;
+
+ dprintf(fd, " %c %s%*c | %*d+ %*d-\n", cp->status,
+ pe->path, pad, ' ', dsa->add_cols + 1, cp->add,
+ dsa->rm_cols + 1, cp->rm);
+ }
+ dprintf(fd,
+ "\n%d file%s changed, %d insertion%s(+), %d deletion%s(-)\n\n",
+ dsa->nfiles, dsa->nfiles > 1 ? "s" : "", dsa->ins,
+ dsa->ins != 1 ? "s" : "", dsa->del, dsa->del != 1 ? "s" : "");
+
+ return NULL;
+}
+
+static const struct got_error *
+print_commit(struct got_commit_object *commit, struct got_object_id *id,
+ struct got_repository *repo, struct got_pathlist_head *changed_paths,
+ struct got_diffstat_cb_arg *diffstat, int fd)
+{
+ const struct got_error *err = NULL;
+ char *id_str, *datestr, *logmsg0, *logmsg, *line;
+ char datebuf[26];
+ time_t committer_time;
+ const char *author, *committer;
+
+ err = got_object_id_str(&id_str, id);
+ if (err)
+ return err;
+
+ dprintf(fd, "commit %s\n", id_str);
+ free(id_str);
+ id_str = NULL;
+ dprintf(fd, "from: %s\n", got_object_commit_get_author(commit));
+ author = got_object_commit_get_author(commit);
+ committer = got_object_commit_get_committer(commit);
+ if (strcmp(author, committer) != 0)
+ dprintf(fd, "via: %s\n", committer);
+ committer_time = got_object_commit_get_committer_time(commit);
+ datestr = get_datestr(&committer_time, datebuf);
+ if (datestr)
+ dprintf(fd, "date: %s UTC\n", datestr);
+ if (got_object_commit_get_nparents(commit) > 1) {
+ const struct got_object_id_queue *parent_ids;
+ struct got_object_qid *qid;
+ int n = 1;
+ parent_ids = got_object_commit_get_parent_ids(commit);
+ STAILQ_FOREACH(qid, parent_ids, entry) {
+ err = got_object_id_str(&id_str, &qid->id);
+ if (err)
+ goto done;
+ dprintf(fd, "parent %d: %s\n", n++, id_str);
+ free(id_str);
+ id_str = NULL;
+ }
+ }
+
+ err = got_object_commit_get_logmsg(&logmsg0, commit);
+ if (err)
+ goto done;
+
+ logmsg = logmsg0;
+ do {
+ line = strsep(&logmsg, "\n");
+ if (line)
+ dprintf(fd, " %s\n", line);
+ } while (line);
+ free(logmsg0);
+
+ err = print_diffstat(diffstat, fd);
+ if (err)
+ goto done;
+
+ if (fsync(fd) == -1 && err == NULL)
+ err = got_error_from_errno("fsync");
+done:
+ free(id_str);
+ return err;
+}
+
+static const struct got_error *
+get_changed_paths(struct got_pathlist_head *paths,
+ struct got_commit_object *commit, struct got_repository *repo,
+ struct got_diffstat_cb_arg *dsa)
+{
+ const struct got_error *err = NULL;
+ struct got_object_id *tree_id1 = NULL, *tree_id2 = NULL;
+ struct got_tree_object *tree1 = NULL, *tree2 = NULL;
+ struct got_object_qid *qid;
+ got_diff_blob_cb cb = got_diff_tree_collect_changed_paths;
+ FILE *f1 = repo_write.diff.f1, *f2 = repo_write.diff.f2;
+ int fd1 = repo_write.diff.fd1, fd2 = repo_write.diff.fd2;
+
+ if (dsa)
+ cb = got_diff_tree_compute_diffstat;
+
+ err = got_opentemp_truncate(f1);
+ if (err)
+ return err;
+ err = got_opentemp_truncate(f2);
+ if (err)
+ return err;
+ err = got_opentemp_truncatefd(fd1);
+ if (err)
+ return err;
+ err = got_opentemp_truncatefd(fd2);
+ if (err)
+ return err;
+
+ qid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit));
+ if (qid != NULL) {
+ struct got_commit_object *pcommit;
+ err = got_object_open_as_commit(&pcommit, repo,
+ &qid->id);
+ if (err)
+ return err;
+
+ tree_id1 = got_object_id_dup(
+ got_object_commit_get_tree_id(pcommit));
+ if (tree_id1 == NULL) {
+ got_object_commit_close(pcommit);
+ return got_error_from_errno("got_object_id_dup");
+ }
+ got_object_commit_close(pcommit);
+
+ }
+
+ if (tree_id1) {
+ err = got_object_open_as_tree(&tree1, repo, tree_id1);
+ if (err)
+ goto done;
+ }
+
+ tree_id2 = got_object_commit_get_tree_id(commit);
+ err = got_object_open_as_tree(&tree2, repo, tree_id2);
+ if (err)
+ goto done;
+
+ err = got_diff_tree(tree1, tree2, f1, f2, fd1, fd2, "", "", repo,
+ cb, dsa ? (void *)dsa : paths, dsa ? 1 : 0);
+done:
+ if (tree1)
+ got_object_tree_close(tree1);
+ if (tree2)
+ got_object_tree_close(tree2);
+ free(tree_id1);
+ return err;
+}
+
+static const struct got_error *
+print_commits(struct got_object_id *root_id, struct got_object_id *end_id,
+ struct got_repository *repo, int fd)
+{
+ const struct got_error *err;
+ struct got_commit_graph *graph;
+ struct got_object_id_queue reversed_commits;
+ struct got_object_qid *qid;
+ struct got_commit_object *commit = NULL;
+ struct got_pathlist_head changed_paths;
+ int ncommits = 0;
+ const int shortlog_threshold = 50;
+
+ STAILQ_INIT(&reversed_commits);
+ TAILQ_INIT(&changed_paths);
+
+ /* XXX first-parent only for now */
+ err = got_commit_graph_open(&graph, "/", 1);
+ if (err)
+ return err;
+ err = got_commit_graph_iter_start(graph, root_id, repo,
+ check_cancelled, NULL);
+ if (err)
+ goto done;
+ for (;;) {
+ struct got_object_id id;
+
+ err = got_commit_graph_iter_next(&id, graph, repo,
+ check_cancelled, NULL);
+ if (err) {
+ if (err->code == GOT_ERR_ITER_COMPLETED)
+ err = NULL;
+ break;
+ }
+
+ err = got_object_open_as_commit(&commit, repo, &id);
+ if (err)
+ break;
+
+ if (end_id && got_object_id_cmp(&id, end_id) == 0)
+ break;
+
+ err = got_object_qid_alloc(&qid, &id);
+ if (err)
+ break;
+
+ STAILQ_INSERT_HEAD(&reversed_commits, qid, entry);
+ ncommits++;
+ got_object_commit_close(commit);
+
+ if (end_id == NULL)
+ break;
+ }
+
+ STAILQ_FOREACH(qid, &reversed_commits, entry) {
+ struct got_diffstat_cb_arg dsa = { 0, 0, 0, 0, 0, 0,
+ &changed_paths, 0, 0, GOT_DIFF_ALGORITHM_PATIENCE };
+
+ err = got_object_open_as_commit(&commit, repo, &qid->id);
+ if (err)
+ break;
+
+ if (ncommits > shortlog_threshold) {
+ err = print_commit_oneline(commit, &qid->id,
+ repo, fd);
+ if (err)
+ break;
+ } else {
+ err = get_changed_paths(&changed_paths, commit,
+ repo, &dsa);
+ if (err)
+ break;
+ err = print_commit(commit, &qid->id, repo,
+ &changed_paths, &dsa, fd);
+ }
+ got_object_commit_close(commit);
+ commit = NULL;
+ got_pathlist_free(&changed_paths, GOT_PATHLIST_FREE_ALL);
+ }
+done:
+ if (commit)
+ got_object_commit_close(commit);
+ while (!STAILQ_EMPTY(&reversed_commits)) {
+ qid = STAILQ_FIRST(&reversed_commits);
+ STAILQ_REMOVE_HEAD(&reversed_commits, entry);
+ got_object_qid_free(qid);
+ }
+ got_pathlist_free(&changed_paths, GOT_PATHLIST_FREE_ALL);
+ got_commit_graph_close(graph);
+ return err;
+}
+
+static const struct got_error *
+print_tag(struct got_object_id *id,
+ const char *refname, struct got_repository *repo, int fd)
+{
+ const struct got_error *err = NULL;
+ struct got_tag_object *tag = NULL;
+ const char *tagger = NULL;
+ char *id_str = NULL, *tagmsg0 = NULL, *tagmsg, *line, *datestr;
+ char datebuf[26];
+ time_t tagger_time;
+
+ err = got_object_open_as_tag(&tag, repo, id);
+ if (err)
+ return err;
+
+ tagger = got_object_tag_get_tagger(tag);
+ tagger_time = got_object_tag_get_tagger_time(tag);
+ err = got_object_id_str(&id_str,
+ got_object_tag_get_object_id(tag));
+ if (err)
+ goto done;
+
+ dprintf(fd, "tag %s\n", refname);
+ dprintf(fd, "from: %s\n", tagger);
+ datestr = get_datestr(&tagger_time, datebuf);
+ if (datestr)
+ dprintf(fd, "date: %s UTC\n", datestr);
+
+ switch (got_object_tag_get_object_type(tag)) {
+ case GOT_OBJ_TYPE_BLOB:
+ dprintf(fd, "object: %s %s\n", GOT_OBJ_LABEL_BLOB, id_str);
+ break;
+ case GOT_OBJ_TYPE_TREE:
+ dprintf(fd, "object: %s %s\n", GOT_OBJ_LABEL_TREE, id_str);
+ break;
+ case GOT_OBJ_TYPE_COMMIT:
+ dprintf(fd, "object: %s %s\n", GOT_OBJ_LABEL_COMMIT, id_str);
+ break;
+ case GOT_OBJ_TYPE_TAG:
+ dprintf(fd, "object: %s %s\n", GOT_OBJ_LABEL_TAG, id_str);
+ break;
+ default:
+ break;
+ }
+
+ tagmsg0 = strdup(got_object_tag_get_message(tag));
+ if (tagmsg0 == NULL) {
+ err = got_error_from_errno("strdup");
+ goto done;
+ }
+ tagmsg = tagmsg0;
+ do {
+ line = strsep(&tagmsg, "\n");
+ if (line)
+ dprintf(fd, " %s\n", line);
+ } while (line);
+ free(tagmsg0);
+done:
+ if (tag)
+ got_object_tag_close(tag);
+ free(id_str);
+ return err;
+}
+
+static const struct got_error *
+notify_changed_ref(const char *refname, uint8_t *old_sha1,
+ uint8_t *new_sha1, struct gotd_imsgev *iev, int fd)
+{
+ const struct got_error *err;
+ struct got_object_id old_id, new_id;
+ int old_obj_type, new_obj_type;
+ const char *label;
+ char *new_id_str = NULL;
+
+ memset(&old_id, 0, sizeof(old_id));
+ memcpy(old_id.sha1, old_sha1, sizeof(old_id.sha1));
+ memset(&new_id, 0, sizeof(new_id));
+ memcpy(new_id.sha1, new_sha1, sizeof(new_id.sha1));
+
+ err = got_object_get_type(&old_obj_type, repo_write.repo, &old_id);
+ if (err)
+ return err;
+
+ err = got_object_get_type(&new_obj_type, repo_write.repo, &new_id);
+ if (err)
+ return err;
+
+ switch (new_obj_type) {
+ case GOT_OBJ_TYPE_COMMIT:
+ err = print_commits(&new_id,
+ old_obj_type == GOT_OBJ_TYPE_COMMIT ? &old_id : NULL,
+ repo_write.repo, fd);
+ break;
+ case GOT_OBJ_TYPE_TAG:
+ err = print_tag(&new_id, refname, repo_write.repo, fd);
+ break;
+ default:
+ err = got_object_type_label(&label, new_obj_type);
+ if (err)
+ goto done;
+ err = got_object_id_str(&new_id_str, &new_id);
+ if (err)
+ goto done;
+ dprintf(fd, "%s: %s object %s\n", refname, label, new_id_str);
+ break;
+ }
+done:
+ free(new_id_str);
+ return err;
+}
+
+static const struct got_error *
+notify_created_ref(const char *refname, uint8_t *sha1,
+ struct gotd_imsgev *iev, int fd)
+{
+ const struct got_error *err;
+ struct got_object_id id;
+ int obj_type;
+
+ memset(&id, 0, sizeof(id));
+ memcpy(id.sha1, sha1, sizeof(id.sha1));
+
+ err = got_object_get_type(&obj_type, repo_write.repo, &id);
+ if (err)
+ return err;
+
+ if (obj_type == GOT_OBJ_TYPE_TAG)
+ return print_tag(&id, refname, repo_write.repo, fd);
+
+ return print_commits(&id, NULL, repo_write.repo, fd);
+}
+
+static const struct got_error *
+render_notification(struct imsg *imsg, struct gotd_imsgev *iev)
+{
+ const struct got_error *err = NULL;
+ struct gotd_imsg_notification_content ireq;
+ size_t datalen, len;
+ char *refname;
+ struct ibuf *wbuf;
+ int fd;
+
+ fd = imsg_get_fd(imsg);
+ if (fd == -1)
+ return got_error(GOT_ERR_PRIVSEP_NO_FD);
+
+ datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+ if (datalen < sizeof(ireq))
+ return got_error(GOT_ERR_PRIVSEP_LEN);
+
+ memcpy(&ireq, imsg->data, sizeof(ireq));
+
+ if (datalen != sizeof(ireq) + ireq.refname_len)
+ return got_error(GOT_ERR_PRIVSEP_LEN);
+
+ refname = strndup(imsg->data + sizeof(ireq), ireq.refname_len);
+ if (refname == NULL)
+ return got_error_from_errno("strndup");
+
+ switch (ireq.action) {
+ case GOTD_NOTIF_ACTION_CREATED:
+ err = notify_created_ref(refname, ireq.new_id, iev, fd);
+ break;
+ case GOTD_NOTIF_ACTION_REMOVED:
+ err = notify_removed_ref(refname, ireq.old_id, iev, fd);
+ break;
+ case GOTD_NOTIF_ACTION_CHANGED:
+ err = notify_changed_ref(refname, ireq.old_id, ireq.new_id,
+ iev, fd);
+ break;
+ }
+
+ if (fsync(fd) == -1) {
+ err = got_error_from_errno("fsync");
+ goto done;
+ }
+
+ len = sizeof(ireq) + ireq.refname_len;
+ wbuf = imsg_create(&iev->ibuf, GOTD_IMSG_NOTIFY, PROC_REPO_WRITE,
+ repo_write.pid, len);
+ if (wbuf == NULL) {
+ err = got_error_from_errno("imsg_create REF");
+ goto done;
+ }
+ if (imsg_add(wbuf, &ireq, sizeof(ireq)) == -1) {
+ err = got_error_from_errno("imsg_add NOTIFY");
+ goto done;
+ }
+ if (imsg_add(wbuf, refname, ireq.refname_len) == -1) {
+ err = got_error_from_errno("imsg_add NOTIFY");
+ goto done;
+ }
+
+ imsg_close(&iev->ibuf, wbuf);
+ gotd_imsg_event_add(iev);
+done:
+ free(refname);
+ if (close(fd) == -1 && err == NULL)
+ err = got_error_from_errno("close");
+ return err;
+}
+
static void
repo_write_dispatch_session(int fd, short event, void *arg)
{
err = update_refs(iev);
if (err) {
log_warnx("update refs: %s", err->msg);
+ }
+ break;
+ case GOTD_IMSG_NOTIFY:
+ err = render_notification(&imsg, iev);
+ if (err) {
+ log_warnx("render notification: %s", err->msg);
+ shut = 1;
}
break;
default:
void
repo_write_main(const char *title, const char *repo_path,
int *pack_fds, int *temp_fds,
+ FILE *diff_f1, FILE *diff_f2, int diff_fd1, int diff_fd2,
struct got_pathlist_head *protected_tag_namespaces,
struct got_pathlist_head *protected_branch_namespaces,
struct got_pathlist_head *protected_branches)
repo_write.protected_tag_namespaces = protected_tag_namespaces;
repo_write.protected_branch_namespaces = protected_branch_namespaces;
repo_write.protected_branches = protected_branches;
+ repo_write.diff.f1 = diff_f1;
+ repo_write.diff.f2 = diff_f2;
+ repo_write.diff.fd1 = diff_fd1;
+ repo_write.diff.fd2 = diff_fd2;
STAILQ_INIT(&repo_write_client.ref_updates);
event_dispatch();
done:
+ if (fclose(diff_f1) == EOF && err == NULL)
+ err = got_error_from_errno("fclose");
+ if (fclose(diff_f2) == EOF && err == NULL)
+ err = got_error_from_errno("fclose");
+ if (close(diff_fd1) == -1 && err == NULL)
+ err = got_error_from_errno("close");
+ if (close(diff_fd2) == -1 && err == NULL)
+ err = got_error_from_errno("close");
if (err)
log_warnx("%s: %s", title, err->msg);
repo_write_shutdown();
blob - e8192eec3947ce83dcedba9e20048cb0ff7dfc76
blob + 6d09a0d009bafa745ad7d7fa491e5c6bcfe4149e
--- gotd/repo_write.h
+++ gotd/repo_write.h
*/
void repo_write_main(const char *, const char *, int *, int *,
+ FILE *, FILE *, int, int,
struct got_pathlist_head *, struct got_pathlist_head *,
struct got_pathlist_head *);
void repo_write_shutdown(void);
blob - 9754b5ea953bf492a5912577904d94d66b1426a8
blob + d764c20d06776aa305d7f4c3e4a40a71cbd913f9
--- gotd/session.c
+++ gotd/session.c
#include "log.h"
#include "session.h"
+struct gotd_session_notif {
+ STAILQ_ENTRY(gotd_session_notif) entry;
+ int fd;
+ enum gotd_notification_action action;
+ char *refname;
+ struct got_object_id old_id;
+ struct got_object_id new_id;
+};
+STAILQ_HEAD(gotd_session_notifications, gotd_session_notif) notifications;
static struct gotd_session {
pid_t pid;
const char *title;
struct got_repository *repo;
+ struct gotd_repo *repo_cfg;
int *pack_fds;
int *temp_fds;
struct gotd_imsgev parent_iev;
+ struct gotd_imsgev notifier_iev;
struct timeval request_timeout;
enum gotd_procid proc_id;
} gotd_session;
struct event tmo;
uid_t euid;
gid_t egid;
+ char *username;
char *packfile_path;
char *packidx_path;
int nref_updates;
return got_error(GOT_ERR_PRIVSEP_MSG);
client->nref_updates = istart.nref_updates;
+ return NULL;
+}
+
+static const struct got_error *
+validate_namespace(const char *namespace)
+{
+ size_t len = strlen(namespace);
+
+ if (len < 5 || strncmp("refs/", namespace, 5) != 0 ||
+ namespace[len - 1] != '/') {
+ return got_error_fmt(GOT_ERR_BAD_REF_NAME,
+ "reference namespace '%s'", namespace);
+ }
+
return NULL;
}
static const struct got_error *
+queue_notification(struct got_object_id *old_id, struct got_object_id *new_id,
+ struct got_repository *repo, struct got_reference *ref)
+{
+ const struct got_error *err = NULL;
+ struct gotd_session_client *client = &gotd_session_client;
+ struct gotd_repo *repo_cfg = gotd_session.repo_cfg;
+ struct gotd_imsgev *iev = &client->repo_child_iev;
+ struct got_pathlist_entry *pe;
+ struct gotd_session_notif *notif;
+
+ if (iev->ibuf.fd == -1 ||
+ STAILQ_EMPTY(&repo_cfg->notification_targets))
+ return NULL; /* notifications unused */
+
+ TAILQ_FOREACH(pe, &repo_cfg->notification_refs, entry) {
+ const char *refname = pe->path;
+ if (strcmp(got_ref_get_name(ref), refname) == 0)
+ break;
+ }
+ if (pe == NULL) {
+ TAILQ_FOREACH(pe, &repo_cfg->notification_ref_namespaces,
+ entry) {
+ const char *namespace = pe->path;
+
+ err = validate_namespace(namespace);
+ if (err)
+ return err;
+ if (strncmp(namespace, got_ref_get_name(ref),
+ strlen(namespace)) == 0)
+ break;
+ }
+ }
+
+ /*
+ * If a branch or a reference namespace was specified in the
+ * configuration file then only send notifications if a match
+ * was found.
+ */
+ if (pe == NULL && (!TAILQ_EMPTY(&repo_cfg->notification_refs) ||
+ !TAILQ_EMPTY(&repo_cfg->notification_ref_namespaces)))
+ return NULL;
+
+ notif = calloc(1, sizeof(*notif));
+ if (notif == NULL)
+ return got_error_from_errno("calloc");
+
+ notif->fd = -1;
+
+ if (old_id == NULL)
+ notif->action = GOTD_NOTIF_ACTION_CREATED;
+ else if (new_id == NULL)
+ notif->action = GOTD_NOTIF_ACTION_REMOVED;
+ else
+ notif->action = GOTD_NOTIF_ACTION_CHANGED;
+
+ if (old_id != NULL)
+ memcpy(¬if->old_id, old_id, sizeof(notif->old_id));
+ if (new_id != NULL)
+ memcpy(¬if->new_id, new_id, sizeof(notif->new_id));
+
+ notif->refname = strdup(got_ref_get_name(ref));
+ if (notif->refname == NULL) {
+ err = got_error_from_errno("strdup");
+ goto done;
+ }
+
+ STAILQ_INSERT_TAIL(¬ifications, notif, entry);
+done:
+ if (err && notif) {
+ free(notif->refname);
+ free(notif);
+ }
+ return err;
+}
+
+/* Forward notification content to the NOTIFY process. */
+static const struct got_error *
+forward_notification(struct gotd_session_client *client, struct imsg *imsg)
+{
+ const struct got_error *err = NULL;
+ struct gotd_imsgev *iev = &gotd_session.notifier_iev;
+ struct gotd_session_notif *notif;
+ struct gotd_imsg_notification_content icontent;
+ char *refname = NULL;
+ size_t datalen;
+ struct gotd_imsg_notify inotify;
+ const char *action;
+
+ memset(&inotify, 0, sizeof(inotify));
+
+ datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+ if (datalen < sizeof(icontent))
+ return got_error(GOT_ERR_PRIVSEP_LEN);
+ memcpy(&icontent, imsg->data, sizeof(icontent));
+ if (datalen != sizeof(icontent) + icontent.refname_len)
+ return got_error(GOT_ERR_PRIVSEP_LEN);
+ refname = strndup(imsg->data + sizeof(icontent), icontent.refname_len);
+ if (refname == NULL)
+ return got_error_from_errno("strndup");
+
+ notif = STAILQ_FIRST(¬ifications);
+ if (notif == NULL)
+ return got_error(GOT_ERR_PRIVSEP_MSG);
+
+ STAILQ_REMOVE(¬ifications, notif, gotd_session_notif, entry);
+
+ if (notif->action != icontent.action || notif->fd == -1 ||
+ strcmp(notif->refname, refname) != 0) {
+ err = got_error(GOT_ERR_PRIVSEP_MSG);
+ goto done;
+ }
+ if (notif->action == GOTD_NOTIF_ACTION_CREATED) {
+ if (memcmp(notif->new_id.sha1, icontent.new_id,
+ SHA1_DIGEST_LENGTH) != 0) {
+ err = got_error_msg(GOT_ERR_PRIVSEP_MSG,
+ "received notification content for unknown event");
+ goto done;
+ }
+ } else if (notif->action == GOTD_NOTIF_ACTION_REMOVED) {
+ if (memcmp(notif->old_id.sha1, icontent.old_id,
+ SHA1_DIGEST_LENGTH) != 0) {
+ err = got_error_msg(GOT_ERR_PRIVSEP_MSG,
+ "received notification content for unknown event");
+ goto done;
+ }
+ } else if (memcmp(notif->old_id.sha1, icontent.old_id,
+ SHA1_DIGEST_LENGTH) != 0 ||
+ memcmp(notif->new_id.sha1, icontent.new_id,
+ SHA1_DIGEST_LENGTH) != 0) {
+ err = got_error_msg(GOT_ERR_PRIVSEP_MSG,
+ "received notification content for unknown event");
+ goto done;
+ }
+
+ switch (notif->action) {
+ case GOTD_NOTIF_ACTION_CREATED:
+ action = "created";
+ break;
+ case GOTD_NOTIF_ACTION_REMOVED:
+ action = "removed";
+ break;
+ case GOTD_NOTIF_ACTION_CHANGED:
+ action = "changed";
+ break;
+ default:
+ err = got_error(GOT_ERR_PRIVSEP_MSG);
+ goto done;
+ }
+
+ strlcpy(inotify.repo_name, gotd_session.repo_cfg->name,
+ sizeof(inotify.repo_name));
+
+ snprintf(inotify.subject_line, sizeof(inotify.subject_line),
+ "%s: %s %s %s", gotd_session.repo_cfg->name,
+ client->username, action, notif->refname);
+
+ if (gotd_imsg_compose_event(iev, GOTD_IMSG_NOTIFY,
+ PROC_SESSION_WRITE, notif->fd, &inotify, sizeof(inotify))
+ == -1) {
+ err = got_error_from_errno("imsg compose NOTIFY");
+ goto done;
+ }
+ notif->fd = -1;
+done:
+ if (notif->fd != -1)
+ close(notif->fd);
+ free(notif);
+ free(refname);
+ return err;
+}
+
+/* Request notification content from REPO_WRITE process. */
+static const struct got_error *
+request_notification(struct gotd_session_notif *notif)
+{
+ const struct got_error *err = NULL;
+ struct gotd_session_client *client = &gotd_session_client;
+ struct gotd_imsgev *iev = &client->repo_child_iev;
+ struct gotd_imsg_notification_content icontent;
+ struct ibuf *wbuf;
+ size_t len;
+ int fd;
+
+ fd = got_opentempfd();
+ if (fd == -1)
+ return got_error_from_errno("got_opentemp");
+
+ memset(&icontent, 0, sizeof(icontent));
+ icontent.client_id = client->id;
+
+ icontent.action = notif->action;
+ memcpy(&icontent.old_id, ¬if->old_id, sizeof(notif->old_id));
+ memcpy(&icontent.new_id, ¬if->new_id, sizeof(notif->new_id));
+ icontent.refname_len = strlen(notif->refname);
+
+ len = sizeof(icontent) + icontent.refname_len;
+ wbuf = imsg_create(&iev->ibuf, GOTD_IMSG_NOTIFY,
+ gotd_session.proc_id, gotd_session.pid, len);
+ if (wbuf == NULL) {
+ err = got_error_from_errno("imsg_create NOTIFY");
+ goto done;
+ }
+ if (imsg_add(wbuf, &icontent, sizeof(icontent)) == -1) {
+ err = got_error_from_errno("imsg_add NOTIFY");
+ goto done;
+ }
+ if (imsg_add(wbuf, notif->refname, icontent.refname_len) == -1) {
+ err = got_error_from_errno("imsg_add NOTIFY");
+ goto done;
+ }
+
+ notif->fd = dup(fd);
+ if (notif->fd == -1) {
+ err = got_error_from_errno("dup");
+ goto done;
+ }
+
+ ibuf_fd_set(wbuf, fd);
+ fd = -1;
+
+ imsg_close(&iev->ibuf, wbuf);
+ gotd_imsg_event_add(iev);
+done:
+ if (err && fd != -1)
+ close(fd);
+ return err;
+}
+
+static const struct got_error *
update_ref(int *shut, struct gotd_session_client *client,
const char *repo_path, struct imsg *imsg)
{
struct got_reference *ref = NULL;
struct gotd_imsg_ref_update iref;
struct got_object_id old_id, new_id;
+ struct gotd_session_notif *notif;
struct got_object_id *id = NULL;
char *refname = NULL;
size_t datalen;
err = got_ref_write(ref, repo); /* will lock/unlock */
if (err)
goto done;
+ err = queue_notification(NULL, &new_id, repo, ref);
+ if (err)
+ goto done;
} else {
err = got_ref_resolve(&id, repo, ref);
if (err)
err = got_ref_delete(ref, repo);
if (err)
goto done;
-
+ err = queue_notification(&old_id, NULL, repo, ref);
+ if (err)
+ goto done;
free(id);
id = NULL;
} else {
err = got_ref_change_ref(ref, &new_id);
if (err)
goto done;
-
err = got_ref_write(ref, repo);
if (err)
goto done;
+ err = queue_notification(&old_id, &new_id, repo, ref);
+ if (err)
+ goto done;
}
free(id);
client->nref_updates--;
if (client->nref_updates == 0) {
send_refs_updated(client);
- client->flush_disconnect = 1;
+ notif = STAILQ_FIRST(¬ifications);
+ if (notif) {
+ client->state = GOTD_STATE_NOTIFY;
+ err = request_notification(notif);
+ if (err) {
+ log_warn("could not send notification: "
+ "%s", err->msg);
+ client->flush_disconnect = 1;
+ }
+ } else
+ client->flush_disconnect = 1;
}
}
return err;
}
+static const struct got_error *
+recv_notification_content(uint32_t *client_id, struct imsg *imsg)
+{
+ struct gotd_imsg_notification_content inotif;
+ size_t datalen;
+
+ datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+ if (datalen < sizeof(inotif))
+ return got_error(GOT_ERR_PRIVSEP_LEN);
+ memcpy(&inotif, imsg->data, sizeof(inotif));
+
+ *client_id = inotif.client_id;
+ return NULL;
+}
+
static void
session_dispatch_repo_child(int fd, short event, void *arg)
{
uint32_t client_id = 0;
int do_disconnect = 0;
int do_ref_updates = 0, do_ref_update = 0;
- int do_packfile_install = 0;
+ int do_packfile_install = 0, do_notify = 0;
if ((n = imsg_get(ibuf, &imsg)) == -1)
fatal("%s: imsg_get error", __func__);
if (err == NULL)
do_ref_update = 1;
break;
+ case GOTD_IMSG_NOTIFY:
+ err = recv_notification_content(&client_id, &imsg);
+ if (err == NULL)
+ do_notify = 1;
+ break;
default:
log_debug("unexpected imsg %d", imsg.hdr.type);
break;
else
disconnect(client);
} else {
+ struct gotd_session_notif *notif;
+
if (do_packfile_install)
err = install_pack(client,
gotd_session.repo->path, &imsg);
else if (do_ref_update)
err = update_ref(&shut, client,
gotd_session.repo->path, &imsg);
+ else if (do_notify)
+ err = forward_notification(client, &imsg);
if (err)
log_warnx("uid %d: %s", client->euid, err->msg);
+
+ notif = STAILQ_FIRST(¬ifications);
+ if (notif && do_notify) {
+ /* Request content for next notification. */
+ err = request_notification(notif);
+ if (err) {
+ log_warn("could not send notification: "
+ "%s", err->msg);
+ shut = 1;
+ }
+ }
}
imsg_free(&imsg);
}
return got_error(GOT_ERR_PRIVSEP_MSG);
datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
- if (datalen != sizeof(iconnect))
+ if (datalen < sizeof(iconnect))
return got_error(GOT_ERR_PRIVSEP_LEN);
memcpy(&iconnect, imsg->data, sizeof(iconnect));
+ if (iconnect.username_len == 0 ||
+ datalen != sizeof(iconnect) + iconnect.username_len)
+ return got_error(GOT_ERR_PRIVSEP_LEN);
client->euid = iconnect.euid;
client->egid = iconnect.egid;
if (client->fd == -1)
return got_error(GOT_ERR_PRIVSEP_NO_FD);
+ client->username = strndup(imsg->data + sizeof(iconnect),
+ iconnect.username_len);
+ if (client->username == NULL)
+ return got_error_from_errno("strndup");
+
imsg_init(&client->iev.ibuf, client->fd);
client->iev.handler = session_dispatch_client;
client->iev.events = EV_READ;
return NULL;
}
+static void
+session_dispatch_notifier(int fd, short event, void *arg)
+{
+ const struct got_error *err;
+ struct gotd_session_client *client = &gotd_session_client;
+ struct gotd_imsgev *iev = arg;
+ struct imsgbuf *ibuf = &iev->ibuf;
+ ssize_t n;
+ int shut = 0;
+ struct imsg imsg;
+ struct gotd_session_notif *notif;
+
+ if (event & EV_READ) {
+ if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+ fatal("imsg_read error");
+ if (n == 0) {
+ /* Connection closed. */
+ shut = 1;
+ goto done;
+ }
+ }
+
+ if (event & EV_WRITE) {
+ n = msgbuf_write(&ibuf->w);
+ if (n == -1 && errno != EAGAIN)
+ fatal("msgbuf_write");
+ if (n == 0) {
+ /* Connection closed. */
+ shut = 1;
+ goto done;
+ }
+ }
+
+ for (;;) {
+ if ((n = imsg_get(ibuf, &imsg)) == -1)
+ fatal("%s: imsg_get error", __func__);
+ if (n == 0) /* No more messages. */
+ break;
+
+ switch (imsg.hdr.type) {
+ case GOTD_IMSG_NOTIFICATION_SENT:
+ if (client->state != GOTD_STATE_NOTIFY) {
+ log_warn("unexpected imsg %d", imsg.hdr.type);
+ break;
+ }
+ notif = STAILQ_FIRST(¬ifications);
+ if (notif == NULL) {
+ disconnect(client);
+ break; /* NOTREACHED */
+ }
+ /* Request content for the next notification. */
+ err = request_notification(notif);
+ if (err) {
+ log_warn("could not send notification: %s",
+ err->msg);
+ disconnect(client);
+ }
+ break;
+ default:
+ log_debug("unexpected imsg %d", imsg.hdr.type);
+ break;
+ }
+
+ imsg_free(&imsg);
+ }
+done:
+ if (!shut) {
+ gotd_imsg_event_add(iev);
+ } else {
+ /* This pipe is dead. Remove its event handler */
+ event_del(&iev->ev);
+ imsg_clear(&iev->ibuf);
+ imsg_init(&iev->ibuf, -1);
+ }
+}
+
static const struct got_error *
+recv_notifier(struct imsg *imsg)
+{
+ struct gotd_imsgev *iev = &gotd_session.notifier_iev;
+ struct gotd_session_client *client = &gotd_session_client;
+ size_t datalen;
+ int fd;
+
+ if (client->state != GOTD_STATE_EXPECT_LIST_REFS)
+ return got_error(GOT_ERR_PRIVSEP_MSG);
+
+ /* We should already have received a pipe to the listener. */
+ if (client->fd == -1)
+ return got_error(GOT_ERR_PRIVSEP_MSG);
+
+ datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+ if (datalen != 0)
+ return got_error(GOT_ERR_PRIVSEP_LEN);
+
+ fd = imsg_get_fd(imsg);
+ if (fd == -1)
+ return NULL; /* notifications unused */
+
+ imsg_init(&iev->ibuf, fd);
+ iev->handler = session_dispatch_notifier;
+ iev->events = EV_READ;
+ iev->handler_arg = NULL;
+ event_set(&iev->ev, iev->ibuf.fd, EV_READ,
+ session_dispatch_notifier, iev);
+ gotd_imsg_event_add(iev);
+
+ return NULL;
+}
+
+static const struct got_error *
recv_repo_child(struct imsg *imsg)
{
struct gotd_imsg_connect_repo_child ichild;
case GOTD_IMSG_DISCONNECT:
do_disconnect = 1;
break;
+ case GOTD_IMSG_CONNECT_NOTIFIER:
+ err = recv_notifier(&imsg);
+ break;
case GOTD_IMSG_CONNECT_REPO_CHILD:
err = recv_repo_child(&imsg);
if (err)
void
session_main(const char *title, const char *repo_path,
int *pack_fds, int *temp_fds, struct timeval *request_timeout,
- enum gotd_procid proc_id)
+ struct gotd_repo *repo_cfg, enum gotd_procid proc_id)
{
const struct got_error *err = NULL;
struct event evsigint, evsigterm, evsighup, evsigusr1;
+ STAILQ_INIT(¬ifications);
+
gotd_session.title = title;
gotd_session.pid = getpid();
gotd_session.pack_fds = pack_fds;
gotd_session.temp_fds = temp_fds;
memcpy(&gotd_session.request_timeout, request_timeout,
sizeof(gotd_session.request_timeout));
+ gotd_session.repo_cfg = repo_cfg;
gotd_session.proc_id = proc_id;
+ imsg_init(&gotd_session.notifier_iev.ibuf, -1);
+
err = got_repo_open(&gotd_session.repo, repo_path, NULL, pack_fds);
if (err)
goto done;
void
gotd_session_shutdown(void)
{
+ struct gotd_session_notif *notif;
+
log_debug("shutting down");
+
+ while (!STAILQ_EMPTY(¬ifications)) {
+ notif = STAILQ_FIRST(¬ifications);
+ STAILQ_REMOVE_HEAD(¬ifications, entry);
+ if (notif->fd != -1)
+ close(notif->fd);
+ free(notif->refname);
+ free(notif);
+ }
+
if (gotd_session.repo)
got_repo_close(gotd_session.repo);
got_repo_pack_fds_close(gotd_session.pack_fds);
got_repo_temp_fds_close(gotd_session.temp_fds);
+ free(gotd_session_client.username);
exit(0);
}
blob - de20117ce268c646687a1977e8e0c7086a7fb2d6
blob + 624f7bb81035da8f1d5293bcbcdf72cd6f064102
--- gotd/session.h
+++ gotd/session.h
*/
void session_main(const char *, const char *, int *, int *, struct timeval *,
- enum gotd_procid);
+ struct gotd_repo *, enum gotd_procid);
blob - ddfc0f1e9d1f0c373066bc33b387387e5beeaf0d
blob + eaca641d2887c0f5e66a410257eaf3fae004a13d
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
test_repo_read_denied_user test_repo_read_denied_group \
test_repo_read_bad_user test_repo_read_bad_group \
test_repo_write test_repo_write_empty test_request_bad \
- test_repo_write_protected
+ test_repo_write_protected test_email_notification
NOOBJ=Yes
CLEANFILES=gotd.conf
GOTD_DEVUSER?=gotdev
GOTD_DEVUSER_HOME!=userinfo $(GOTD_DEVUSER) | awk '/^dir/ {print $$2}'
GOTD_TEST_REPO!?=mktemp -d "$(GOTD_TEST_ROOT)/gotd-test-repo-XXXXXXXXXX"
-GOTD_TEST_REPO_URL=ssh://${GOTD_DEVUSER}@127.0.0.1/test-repo
+GOTD_TEST_REPO_NAME=test-repo
+GOTD_TEST_REPO_URL=ssh://${GOTD_DEVUSER}@127.0.0.1/$(GOTD_TEST_REPO_NAME)
+GOTD_TEST_SMTP_PORT=2525
GOTD_TEST_USER?=${DOAS_USER}
.if empty(GOTD_TEST_USER)
BINDIR ?= ${PREFIX}/bin
.endif
-GOTD_START_CMD?=$(BINDIR)/gotd -vv -f $(PWD)/gotd.conf
+GOTD_START_CMD?=env ${GOTD_ENV} $(BINDIR)/gotd -vv -f $(PWD)/gotd.conf
GOTD_STOP_CMD?=$(BINDIR)/gotctl -f $(GOTD_SOCK) stop
GOTD_TRAP=trap "$(GOTD_STOP_CMD)" HUP INT QUIT PIPE TERM
+GOTD_ENV=GOT_NOTIFY_EMAIL_TIMEOUT=1
+
GOTD_TEST_ENV=GOTD_TEST_ROOT=$(GOTD_TEST_ROOT) \
GOTD_TEST_REPO_URL=$(GOTD_TEST_REPO_URL) \
+ GOTD_TEST_REPO_NAME=$(GOTD_TEST_REPO_NAME) \
GOTD_TEST_REPO=$(GOTD_TEST_REPO) \
GOTD_SOCK=$(GOTD_SOCK) \
GOTD_DEVUSER=$(GOTD_DEVUSER) \
+ GOTD_USER=$(GOTD_USER) \
+ GOTD_TEST_SMTP_PORT=$(GOTD_TEST_SMTP_PORT) \
HOME=$(GOTD_TEST_USER_HOME) \
PATH=$(GOTD_TEST_USER_HOME)/bin:$(PATH)
@$(GOTD_TRAP); $(GOTD_START_CMD)
@$(GOTD_TRAP); sleep .5
+start_gotd_email_notification: ensure_root
+ @echo 'listen on "$(GOTD_SOCK)"' > $(PWD)/gotd.conf
+ @echo "user $(GOTD_USER)" >> $(PWD)/gotd.conf
+ @echo 'repository "test-repo" {' >> $(PWD)/gotd.conf
+ @echo ' path "$(GOTD_TEST_REPO)"' >> $(PWD)/gotd.conf
+ @echo ' permit rw $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
+ @echo ' notify {' >> $(PWD)/gotd.conf
+ @echo -n ' email to ${GOTD_DEVUSER}' >> $(PWD)/gotd.conf
+ @echo ' relay 127.0.0.1 port ${GOTD_TEST_SMTP_PORT}' >> $(PWD)/gotd.conf
+ @echo " }" >> $(PWD)/gotd.conf
+ @echo "}" >> $(PWD)/gotd.conf
+ @$(GOTD_TRAP); $(GOTD_START_CMD)
+ @$(GOTD_TRAP); sleep .5
+
prepare_test_repo: ensure_root
@chown ${GOTD_USER} "${GOTD_TEST_REPO}"
@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./prepare_test_repo.sh'
'env $(GOTD_TEST_ENV) sh ./request_bad.sh'
@$(GOTD_STOP_CMD) 2>/dev/null
+test_email_notification: prepare_test_repo start_gotd_email_notification
+ @-$(GOTD_TRAP); su -m ${GOTD_TEST_USER} -c \
+ 'env $(GOTD_TEST_ENV) sh ./email_notification.sh'
+ @$(GOTD_STOP_CMD) 2>/dev/null
+
.include <bsd.regress.mk>
blob - d75faae64b46a7c347ecb77c6631207a3ded44b0
blob + e91075f7971646d565d0d0c8b6509d711fc2128f
--- regress/gotd/README
+++ regress/gotd/README
The suite must be started as root in order to be able to start and stop gotd.
The test suite switches to non-root users as appropriate.
+
+The test suite uses netcat on port 2525 to test SMTP notifications.
+If this port is already in use then affected tests might fail.
+If needed the port can be overriden on the make command line:
+
+ $ doas make server-regress GOTD_TEST_SMTP_PORT=12345
blob - /dev/null
blob + 44052114a99b0f98370a006d3e3c691db559ceec (mode 644)
--- /dev/null
+++ regress/gotd/email_notification.sh
+#!/bin/sh
+#
+# Copyright (c) 2024 Stefan Sperling <stsp@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ../cmdline/common.sh
+. ./common.sh
+
+test_file_changed() {
+ local testroot=`test_init file_changed 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ echo "change alpha" > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'make changes' > /dev/null)
+ local commit_id=`git_show_head $testroot/repo-clone`
+ local author_time=`git_show_author_time $testroot/repo-clone`
+
+ (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \
+ | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) &
+
+ got send -b main -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for nc -l
+
+ HOSTNAME=`hostname`
+ printf "HELO localhost\r\n" > $testroot/stdout.expected
+ printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \
+ >> $testroot/stdout.expected
+ printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected
+ printf "DATA\r\n" >> $testroot/stdout.expected
+ printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected
+ printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected
+ printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected
+ printf "${GOTD_DEVUSER} changed refs/heads/main\r\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf "commit $commit_id\n" >> $testroot/stdout.expected
+ printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected
+ d=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+ printf "date: $d\n" >> $testroot/stdout.expected
+ printf " \n" >> $testroot/stdout.expected
+ printf " make changes\n \n" >> $testroot/stdout.expected
+ printf " M alpha | 1+ 1-\n\n" >> $testroot/stdout.expected
+ printf "1 file changed, 1 insertion(+), 1 deletion(-)\n\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf ".\r\n" >> $testroot/stdout.expected
+ printf "QUIT\r\n" >> $testroot/stdout.expected
+
+ grep -v ^Date $testroot/stdout > $testroot/stdout.filtered
+ cmp -s $testroot/stdout.expected $testroot/stdout.filtered
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout.filtered
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_many_commits_not_summarized() {
+ local testroot=`test_init many_commits_not_summarized 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ for i in `seq 1 24`; do
+ echo "alpha $i" > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'make changes' > /dev/null)
+ local commit_id=`git_show_head $testroot/repo-clone`
+ local author_time=`git_show_author_time $testroot/repo-clone`
+ d=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+ set -- "$@" "$commit_id $d"
+ done
+
+ (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \
+ | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) &
+
+ got send -b main -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for nc -l
+
+ HOSTNAME=`hostname`
+ printf "HELO localhost\r\n" > $testroot/stdout.expected
+ printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \
+ >> $testroot/stdout.expected
+ printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected
+ printf "DATA\r\n" >> $testroot/stdout.expected
+ printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" \
+ >> $testroot/stdout.expected
+ printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected
+ printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected
+ printf "${GOTD_DEVUSER} changed refs/heads/main\r\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ for i in `seq 1 24`; do
+ s=`pop_idx $i "$@"`
+ commit_id=$(echo $s | cut -d' ' -f1)
+ commit_time=$(echo $s | sed -e "s/^$commit_id //g")
+ printf "commit $commit_id\n" >> $testroot/stdout.expected
+ printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected
+ printf "date: $commit_time\n" >> $testroot/stdout.expected
+ printf " \n" >> $testroot/stdout.expected
+ printf " make changes\n \n" >> $testroot/stdout.expected
+ printf " M alpha | 1+ 1-\n\n" \
+ >> $testroot/stdout.expected
+ printf "1 file changed, 1 insertion(+), 1 deletion(-)\n\n" \
+ >> $testroot/stdout.expected
+ done
+ printf "\r\n" >> $testroot/stdout.expected
+ printf ".\r\n" >> $testroot/stdout.expected
+ printf "QUIT\r\n" >> $testroot/stdout.expected
+
+ grep -v ^Date $testroot/stdout > $testroot/stdout.filtered
+ cmp -s $testroot/stdout.expected $testroot/stdout.filtered
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout.filtered
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_many_commits_summarized() {
+ local testroot=`test_init many_commits_summarized 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ for i in `seq 1 51`; do
+ echo "alpha $i" > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'make changes' > /dev/null)
+ local commit_id=`git_show_head $testroot/repo-clone`
+ local short_commit_id=`trim_obj_id 33 $commit_id`
+ local author_time=`git_show_author_time $testroot/repo-clone`
+ d=`date -u -r $author_time +"%G-%m-%d"`
+ set -- "$@" "$short_commit_id $d"
+ done
+
+ (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \
+ | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) &
+
+ got send -b main -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for nc -l
+
+ HOSTNAME=`hostname`
+ printf "HELO localhost\r\n" > $testroot/stdout.expected
+ printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \
+ >> $testroot/stdout.expected
+ printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected
+ printf "DATA\r\n" >> $testroot/stdout.expected
+ printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" \
+ >> $testroot/stdout.expected
+ printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected
+ printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected
+ printf "${GOTD_DEVUSER} changed refs/heads/main\r\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ for i in `seq 1 51`; do
+ s=`pop_idx $i "$@"`
+ commit_id=$(echo $s | cut -d' ' -f1)
+ commit_time=$(echo $s | sed -e "s/^$commit_id //g")
+ printf "$commit_time $commit_id $GOT_AUTHOR_8 make changes\n" \
+ >> $testroot/stdout.expected
+ done
+ printf "\r\n" >> $testroot/stdout.expected
+ printf ".\r\n" >> $testroot/stdout.expected
+ printf "QUIT\r\n" >> $testroot/stdout.expected
+
+ grep -v ^Date $testroot/stdout > $testroot/stdout.filtered
+ cmp -s $testroot/stdout.expected $testroot/stdout.filtered
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout.filtered
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_branch_created() {
+ local testroot=`test_init branch_created 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ (cd $testroot/wt && got branch newbranch > /dev/null)
+
+ echo "change alpha on branch" > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'newbranch' > /dev/null)
+ local commit_id=`git_show_branch_head $testroot/repo-clone newbranch`
+ local author_time=`git_show_author_time $testroot/repo-clone $commit_id`
+
+ (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \
+ | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) &
+
+ got send -b newbranch -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for nc -l
+
+ HOSTNAME=`hostname`
+ printf "HELO localhost\r\n" > $testroot/stdout.expected
+ printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \
+ >> $testroot/stdout.expected
+ printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected
+ printf "DATA\r\n" >> $testroot/stdout.expected
+ printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected
+ printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected
+ printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected
+ printf "${GOTD_DEVUSER} created refs/heads/newbranch\r\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf "commit $commit_id\n" >> $testroot/stdout.expected
+ printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected
+ d=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+ printf "date: $d\n" >> $testroot/stdout.expected
+ printf " \n" >> $testroot/stdout.expected
+ printf " newbranch\n \n" >> $testroot/stdout.expected
+ printf " M alpha | 1+ 1-\n\n" >> $testroot/stdout.expected
+ printf "1 file changed, 1 insertion(+), 1 deletion(-)\n\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf ".\r\n" >> $testroot/stdout.expected
+ printf "QUIT\r\n" >> $testroot/stdout.expected
+
+ grep -v ^Date $testroot/stdout > $testroot/stdout.filtered
+ cmp -s $testroot/stdout.expected $testroot/stdout.filtered
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout.filtered
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_branch_removed() {
+ local testroot=`test_init branch_removed 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \
+ | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) &
+
+ local commit_id=`git_show_branch_head $testroot/repo-clone newbranch`
+
+ got send -d newbranch -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for nc -l
+
+ HOSTNAME=`hostname`
+ printf "HELO localhost\r\n" > $testroot/stdout.expected
+ printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \
+ >> $testroot/stdout.expected
+ printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected
+ printf "DATA\r\n" >> $testroot/stdout.expected
+ printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected
+ printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected
+ printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected
+ printf "${GOTD_DEVUSER} removed refs/heads/newbranch\r\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf "Removed refs/heads/newbranch: $commit_id\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf ".\r\n" >> $testroot/stdout.expected
+ printf "QUIT\r\n" >> $testroot/stdout.expected
+
+ grep -v ^Date $testroot/stdout > $testroot/stdout.filtered
+ cmp -s $testroot/stdout.expected $testroot/stdout.filtered
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout.filtered
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_tag_created() {
+ local testroot=`test_init tag_created 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got tag -r $testroot/repo-clone -m "new tag" 1.0 > /dev/null
+ local commit_id=`git_show_head $testroot/repo-clone`
+ local tagger_time=`git_show_tagger_time $testroot/repo-clone 1.0`
+
+ (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \
+ | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) &
+
+ got send -t 1.0 -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for nc -l
+
+ HOSTNAME=`hostname`
+ printf "HELO localhost\r\n" > $testroot/stdout.expected
+ printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \
+ >> $testroot/stdout.expected
+ printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected
+ printf "DATA\r\n" >> $testroot/stdout.expected
+ printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected
+ printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected
+ printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected
+ printf "${GOTD_DEVUSER} created refs/tags/1.0\r\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf "tag refs/tags/1.0\n" >> $testroot/stdout.expected
+ printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected
+ d=`date -u -r $tagger_time +"%a %b %e %X %Y UTC"`
+ printf "date: $d\n" >> $testroot/stdout.expected
+ printf "object: commit $commit_id\n" >> $testroot/stdout.expected
+ printf " \n" >> $testroot/stdout.expected
+ printf " new tag\n \n" >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf ".\r\n" >> $testroot/stdout.expected
+ printf "QUIT\r\n" >> $testroot/stdout.expected
+
+ grep -v ^Date $testroot/stdout > $testroot/stdout.filtered
+ cmp -s $testroot/stdout.expected $testroot/stdout.filtered
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout.filtered
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_tag_changed() {
+ local testroot=`test_init tag_changed 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ echo "change alpha" > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'make changes' > /dev/null)
+ local commit_id=`git_show_head $testroot/repo-clone`
+
+ got ref -r $testroot/repo-clone -d refs/tags/1.0 >/dev/null
+ got tag -r $testroot/repo-clone -m "new tag" 1.0 > /dev/null
+ local tagger_time=`git_show_tagger_time $testroot/repo-clone 1.0`
+
+ (printf "220\r\n250\r\n250\r\n250\r\n354\r\n250\r\n221\r\n" \
+ | timeout 5 nc -l "$GOTD_TEST_SMTP_PORT" > $testroot/stdout) &
+
+ got send -f -t 1.0 -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for nc -l
+
+ HOSTNAME=`hostname`
+ printf "HELO localhost\r\n" > $testroot/stdout.expected
+ printf "MAIL FROM:<${GOTD_USER}@${HOSTNAME}>\r\n" \
+ >> $testroot/stdout.expected
+ printf "RCPT TO:<${GOTD_DEVUSER}>\r\n" >> $testroot/stdout.expected
+ printf "DATA\r\n" >> $testroot/stdout.expected
+ printf "From: ${GOTD_USER}@${HOSTNAME}\r\n" >> $testroot/stdout.expected
+ printf "To: ${GOTD_DEVUSER}\r\n" >> $testroot/stdout.expected
+ printf "Subject: $GOTD_TEST_REPO_NAME: " >> $testroot/stdout.expected
+ printf "${GOTD_DEVUSER} changed refs/tags/1.0\r\n" \
+ >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf "tag refs/tags/1.0\n" >> $testroot/stdout.expected
+ printf "from: $GOT_AUTHOR\n" >> $testroot/stdout.expected
+ d=`date -u -r $tagger_time +"%a %b %e %X %Y UTC"`
+ printf "date: $d\n" >> $testroot/stdout.expected
+ printf "object: commit $commit_id\n" >> $testroot/stdout.expected
+ printf " \n" >> $testroot/stdout.expected
+ printf " new tag\n \n" >> $testroot/stdout.expected
+ printf "\r\n" >> $testroot/stdout.expected
+ printf ".\r\n" >> $testroot/stdout.expected
+ printf "QUIT\r\n" >> $testroot/stdout.expected
+
+ grep -v ^Date $testroot/stdout > $testroot/stdout.filtered
+ cmp -s $testroot/stdout.expected $testroot/stdout.filtered
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout.filtered
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_parseargs "$@"
+run_test test_file_changed
+run_test test_many_commits_not_summarized
+run_test test_many_commits_summarized
+run_test test_branch_created
+run_test test_branch_removed
+run_test test_tag_created
+run_test test_tag_changed