diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--MANUAL.txt38
-rw-r--r--README.txt2
-rw-r--r--assets/cgit.css17
-rw-r--r--custom/cgitrc19
-rw-r--r--source/cgit.c16
-rw-r--r--source/cgit.h4
-rw-r--r--source/filter.c3
-rw-r--r--source/filter.h2
-rw-r--r--source/shared.c2
-rw-r--r--source/ui-commit.c128
-rwxr-xr-xtests/t0206-trailers.sh91
11 files changed, 315 insertions, 7 deletions
diff --git a/MANUAL.txt b/MANUAL.txt
index f59a64c..fc91125 100644
--- a/MANUAL.txt
+++ b/MANUAL.txt
@@ -256,6 +256,16 @@ enable-subject-links::
parent commit as link text when generating links to parent commits in
commit view. Default value: "0". See also: "repo.enable-subject-links".
+enable-trailers::
+ Flag which, when set to "1", makes the commit page split the trailer
+ block, the Signed-off-by lines and their kin, off the end of the commit
+ message and show it as a table under the message. The block is found
+ the way git finds it for "git interpret-trailers": the last paragraph
+ is taken when every line in it is a trailer, or when it holds a
+ Signed-off-by and at least a quarter of its lines are trailers.
+ Default value: "0". See also: "trailer-filter",
+ "repo.enable-trailers".
+
enable-tree-group-dirs::
Flag which, when set to "1", makes the tree view list all directories
first, sorted, followed by the files. When set to "0", entries appear in
@@ -512,6 +522,18 @@ summary-tags::
Specifies the number of tags to display in the repository "summary"
view. Default value: "10".
+trailer-filter::
+ Specifies a command which will be invoked to format the value of a
+ commit trailer when "enable-trailers" is set. The command will get the
+ trailer key and an origin page string as its command line arguments and
+ the value on its STDIN, and the STDOUT from the command will be
+ included verbatim as the value. Trailers whose value is a name and
+ email address, such as Signed-off-by, go through the email-filter
+ instead, and when no trailer-filter is configured the remaining values
+ go through the commit-filter. A ready filter linking Fixes, Closes and
+ URL values ships as custom/extensions/link-trailers.lua. Default value:
+ none. See also: "Filter API".
+
virtual-root::
Url which, if specified, will be used as root for all cgit links. It
will also cause cgit to generate 'virtual urls', i.e. urls like
@@ -600,6 +622,10 @@ repo.enable-subject-links::
A flag which can be used to override the global setting
"enable-subject-links". Default value: <enable-subject-links>.
+repo.enable-trailers::
+ A flag which can be used to override the global setting
+ "enable-trailers". Default value: <enable-trailers>.
+
repo.head-content::
This value will be added verbatim to the html HEAD section of each page
displayed for this repo. Default value: none. See also: "head-include".
@@ -687,6 +713,11 @@ repo.source-filter::
"enable-filter-overrides" is set. Default value: <source-filter>. See
also: "Filter API".
+repo.trailer-filter::
+ Override the default trailer-filter. Only honoured when
+ "enable-filter-overrides" is set. Default value: <trailer-filter>. See
+ also: "Filter API".
+
repo.url::
The relative url used to access the repository. This must be the first
setting specified for each repo. Default value: none.
@@ -805,6 +836,13 @@ source filter::
be filtered is available on standard input and the filtered contents is
expected on standard output.
+trailer filter::
+ This filter is given two parameters: the key of the trailer being
+ formatted, such as "Fixes", and a string indicating the originating
+ page. The filter will then receive the trailer value, already HTML
+ escaped, on standard input and is expected to write to standard output
+ the formatted value to be included in the page.
+
All filters are handed the following environment variables:
diff --git a/README.txt b/README.txt
index bba4a46..55f33de 100644
--- a/README.txt
+++ b/README.txt
@@ -79,7 +79,7 @@ and each script's header lists the exact install commands for its own.
lexer set.
* The about-page renderer (`about-render.lua`) needs `lpeg` for markdown and man
pages. Plain text needs only Lua.
-* `link-commits.lua` needs nothing beyond Lua itself.
+* `link-commits.lua` and `link-trailers.lua` need nothing beyond Lua itself.
`luaossl` has no Lua 5.5 build, so build cgit against 5.1 to 5.4 if you use the
auth or email filters.
diff --git a/assets/cgit.css b/assets/cgit.css
index c141e3f..462c4ae 100644
--- a/assets/cgit.css
+++ b/assets/cgit.css
@@ -835,6 +835,23 @@ div#cgit div.commit-msg {
font-family: var(--font-mono);
}
+div#cgit table.commit-trailers {
+ margin-top: 1em;
+}
+
+div#cgit table.commit-trailers th {
+ text-align: left;
+ font-weight: normal;
+ padding: 0.1em 1em 0.1em 0.1em;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+div#cgit table.commit-trailers td {
+ padding: 0.1em 1em 0.1em 0.1em;
+ overflow-wrap: anywhere;
+}
+
div#cgit div.notes-header {
font-weight: bold;
padding-top: 1.5em;
diff --git a/custom/cgitrc b/custom/cgitrc
index 6de214f..7ba1e13 100644
--- a/custom/cgitrc
+++ b/custom/cgitrc
@@ -213,6 +213,11 @@ enable-remote-branches=0
# 1. Default is 0.
enable-subject-links=0
+# Split the trailer block, the Signed-off-by lines and their kin, off the end
+# of a commit message and show it as a table on the commit page. Values are 0
+# or 1. Default is 0.
+enable-trailers=0
+
# Allow /plain to serve HTML mimetypes instead of downgrading to text/plain.
# Values are 0 or 1. Default is 0.
enable-html-serving=0
@@ -274,6 +279,11 @@ enable-filter-overrides=0
# Default is none.
#email-filter=lua:/usr/local/lib/cgit/filters/email-gravatar.lua
+# Filter command used to format the value of a commit trailer when
+# enable-trailers is 1. The shipped filter links Fixes, Closes and URL values.
+# Value is a command optionally prefixed with exec or lua. Default is none.
+#trailer-filter=lua:/usr/local/lib/cgit/filters/link-trailers.lua
+
# Filter command used to format plaintext blobs in the tree view. The shipped
# filter uses the Scintillua lexers and needs lpeg, and missing either means
# plain uncolored text, not an error. Value is a command optionally prefixed
@@ -495,6 +505,10 @@ enable-plain-email=1
# global enable-subject-links value.
#repo.enable-subject-links=0
+# Per-repo override of enable-trailers. Values are 0 or 1. Default is the
+# global enable-trailers value.
+#repo.enable-trailers=0
+
# Per-repo override of enable-html-serving. Values are 0 or 1. Default is the
# global enable-html-serving value.
#repo.enable-html-serving=0
@@ -530,3 +544,8 @@ enable-plain-email=1
# enable-filter-overrides is 1. Value is a command. Default is the global
# email-filter value.
#repo.email-filter=lua:/usr/local/lib/cgit/filters/email-gravatar.lua
+
+# Per-repo override of the trailer-filter. Only honoured when
+# enable-filter-overrides is 1. Value is a command. Default is the global
+# trailer-filter value.
+#repo.trailer-filter=lua:/usr/local/lib/cgit/filters/link-trailers.lua
diff --git a/source/cgit.c b/source/cgit.c
index e6e8613..ae91dd2 100644
--- a/source/cgit.c
+++ b/source/cgit.c
@@ -247,6 +247,8 @@ static void print_repo(FILE *f, struct cgit_repo *repo)
cgit_fprintf_filter(repo->source_filter, f, "repo.source-filter=");
if (repo->email_filter && repo->email_filter != ctx.cfg.email_filter)
cgit_fprintf_filter(repo->email_filter, f, "repo.email-filter=");
+ if (repo->trailer_filter && repo->trailer_filter != ctx.cfg.trailer_filter)
+ cgit_fprintf_filter(repo->trailer_filter, f, "repo.trailer-filter=");
if (repo->snapshots != ctx.cfg.snapshots) {
char *formats = build_snapshot_setting(repo->snapshots);
fprintf(f, "repo.snapshots=%s\n", formats ? formats : "");
@@ -262,6 +264,7 @@ static void print_repo(FILE *f, struct cgit_repo *repo)
fprintf(f, "repo.logo-link=%s\n", repo->logo_link);
fprintf(f, "repo.enable-remote-branches=%d\n", repo->enable_remote_branches);
fprintf(f, "repo.enable-subject-links=%d\n", repo->enable_subject_links);
+ fprintf(f, "repo.enable-trailers=%d\n", repo->enable_trailers);
fprintf(f, "repo.enable-html-serving=%d\n", repo->enable_html_serving);
if (repo->branch_sort == 1)
fprintf(f, "repo.branch-sort=age\n");
@@ -560,6 +563,8 @@ static void apply_config(const char *name, const char *value)
ctx.cfg.enable_remote_branches = atoi(value);
else if (!strcmp(name, "enable-subject-links"))
ctx.cfg.enable_subject_links = atoi(value);
+ else if (!strcmp(name, "enable-trailers"))
+ ctx.cfg.enable_trailers = atoi(value);
else if (!strcmp(name, "enable-html-serving"))
ctx.cfg.enable_html_serving = atoi(value);
else if (!strcmp(name, "enable-tree-linenumbers"))
@@ -598,6 +603,8 @@ static void apply_config(const char *name, const char *value)
ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT);
else if (!strcmp(name, "email-filter"))
ctx.cfg.email_filter = cgit_new_filter(value, EMAIL);
+ else if (!strcmp(name, "trailer-filter"))
+ ctx.cfg.trailer_filter = cgit_new_filter(value, TRAILER);
else if (!strcmp(name, "auth-filter"))
ctx.cfg.auth_filter = cgit_new_filter(value, AUTH);
else if (!strcmp(name, "embedded"))
@@ -1165,6 +1172,8 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu
repo->enable_remote_branches = atoi(value);
else if (!strcmp(name, "enable-subject-links"))
repo->enable_subject_links = atoi(value);
+ else if (!strcmp(name, "enable-trailers"))
+ repo->enable_trailers = atoi(value);
else if (!strcmp(name, "enable-html-serving"))
repo->enable_html_serving = atoi(value);
else if (!strcmp(name, "branch-sort")) {
@@ -1204,7 +1213,8 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu
!strcmp(name, "about-filter") ||
!strcmp(name, "commit-filter") ||
!strcmp(name, "source-filter") ||
- !strcmp(name, "email-filter")
+ !strcmp(name, "email-filter") ||
+ !strcmp(name, "trailer-filter")
) {
if (!ctx.cfg.enable_filter_overrides)
fprintf(stderr, "[cgit] Ignoring repo %s: enable-filter-overrides is not set\n", name);
@@ -1214,8 +1224,10 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu
repo->commit_filter = cgit_new_filter(value, COMMIT);
else if (!strcmp(name, "source-filter"))
repo->source_filter = cgit_new_filter(value, SOURCE);
- else
+ else if (!strcmp(name, "email-filter"))
repo->email_filter = cgit_new_filter(value, EMAIL);
+ else
+ repo->trailer_filter = cgit_new_filter(value, TRAILER);
} else
fprintf(stderr, "[cgit] Unknown repo config key: %s\n", name);
}
diff --git a/source/cgit.h b/source/cgit.h
index cc4ac02..d976906 100644
--- a/source/cgit.h
+++ b/source/cgit.h
@@ -90,6 +90,7 @@ struct cgit_repo {
int enable_log_linecount;
int enable_remote_branches;
int enable_subject_links;
+ int enable_trailers;
int enable_html_serving;
int max_stats;
int branch_sort;
@@ -99,6 +100,7 @@ struct cgit_repo {
struct cgit_filter *commit_filter;
struct cgit_filter *source_filter;
struct cgit_filter *email_filter;
+ struct cgit_filter *trailer_filter;
struct string_list submodules;
int hide;
int ignore;
@@ -228,6 +230,7 @@ struct cgit_config {
int enable_relative_dates;
int enable_remote_branches;
int enable_subject_links;
+ int enable_trailers;
int enable_html_serving;
int enable_tree_linenumbers;
int enable_tree_group_dirs;
@@ -263,6 +266,7 @@ struct cgit_config {
struct cgit_filter *commit_filter;
struct cgit_filter *source_filter;
struct cgit_filter *email_filter;
+ struct cgit_filter *trailer_filter;
struct cgit_filter *auth_filter;
};
diff --git a/source/filter.c b/source/filter.c
index 2731d58..81c4681 100644
--- a/source/filter.c
+++ b/source/filter.c
@@ -395,12 +395,14 @@ void cgit_cleanup_filters(void)
cleanup_filter(ctx.cfg.commit_filter);
cleanup_filter(ctx.cfg.source_filter);
cleanup_filter(ctx.cfg.email_filter);
+ cleanup_filter(ctx.cfg.trailer_filter);
cleanup_filter(ctx.cfg.auth_filter);
for (i = 0; i < cgit_repolist.count; ++i) {
cleanup_filter(cgit_repolist.repos[i].about_filter);
cleanup_filter(cgit_repolist.repos[i].commit_filter);
cleanup_filter(cgit_repolist.repos[i].source_filter);
cleanup_filter(cgit_repolist.repos[i].email_filter);
+ cleanup_filter(cgit_repolist.repos[i].trailer_filter);
}
}
@@ -438,6 +440,7 @@ struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype)
argument_count = 12;
break;
case EMAIL:
+ case TRAILER:
argument_count = 2;
break;
case SOURCE:
diff --git a/source/filter.h b/source/filter.h
index 861578c..fa43416 100644
--- a/source/filter.h
+++ b/source/filter.h
@@ -13,7 +13,7 @@
#include <stdio.h>
typedef enum {
- ABOUT, COMMIT, SOURCE, EMAIL, AUTH
+ ABOUT, COMMIT, SOURCE, EMAIL, TRAILER, AUTH
} filter_type;
struct cgit_filter {
diff --git a/source/shared.c b/source/shared.c
index beda48b..59712ce 100644
--- a/source/shared.c
+++ b/source/shared.c
@@ -215,6 +215,7 @@ struct cgit_repo *cgit_add_repo(const char *url)
repo->enable_log_linecount = ctx.cfg.enable_log_linecount;
repo->enable_remote_branches = ctx.cfg.enable_remote_branches;
repo->enable_subject_links = ctx.cfg.enable_subject_links;
+ repo->enable_trailers = ctx.cfg.enable_trailers;
repo->enable_html_serving = ctx.cfg.enable_html_serving;
repo->max_stats = ctx.cfg.max_stats;
repo->branch_sort = ctx.cfg.branch_sort;
@@ -226,6 +227,7 @@ struct cgit_repo *cgit_add_repo(const char *url)
repo->commit_filter = ctx.cfg.commit_filter;
repo->source_filter = ctx.cfg.source_filter;
repo->email_filter = ctx.cfg.email_filter;
+ repo->trailer_filter = ctx.cfg.trailer_filter;
repo->clone_url = ctx.cfg.clone_url;
repo->submodules.strdup_strings = 1;
repo->hide = repo->ignore = 0;
diff --git a/source/ui-commit.c b/source/ui-commit.c
index 6055509..b67f5db 100644
--- a/source/ui-commit.c
+++ b/source/ui-commit.c
@@ -18,6 +18,9 @@
#include "ui-log.h"
#include "ui-shared.h"
+#include <list.h>
+#include <trailer.h>
+
// The diff below the message is always taken against the first parent alone,
// which says little about a merge of many branches, so a commit with this
// many parents or more is shown without a diff at all.
@@ -76,6 +79,127 @@ static void print_filtered_text(const char *text)
cgit_close_filter(ctx.repo->commit_filter);
}
+/*
+ * A value shaped like an ident is written the way the author row is, through
+ * the email filter. Anything else goes through the trailer filter when one is
+ * set, and otherwise through the commit filter, so the links a site already
+ * draws in message text keep appearing on trailer lines.
+ */
+static void print_trailer_value(const char *key, const char *value)
+{
+ struct ident_split ident;
+ struct strbuf name = STRBUF_INIT, email = STRBUF_INIT;
+
+ if (!split_ident_line(&ident, value, strlen(value)) &&
+ memchr(ident.mail_begin, '@', ident.mail_end - ident.mail_begin)) {
+ strbuf_add(&name, ident.name_begin, ident.name_end - ident.name_begin);
+ strbuf_addch(&email, '<');
+ strbuf_add(&email, ident.mail_begin, ident.mail_end - ident.mail_begin);
+ strbuf_addch(&email, '>');
+ cgit_open_filter(ctx.repo->email_filter, email.buf, "commit");
+ html_txt(name.buf);
+ if (ctx.cfg.enable_plain_email) {
+ html(" ");
+ html_txt(email.buf);
+ }
+ cgit_close_filter(ctx.repo->email_filter);
+ strbuf_release(&name);
+ strbuf_release(&email);
+ } else if (ctx.repo->trailer_filter) {
+ cgit_open_filter(ctx.repo->trailer_filter, key, "commit");
+ html_txt(value);
+ cgit_close_filter(ctx.repo->trailer_filter);
+ } else
+ print_filtered_text(value);
+}
+
+/*
+ * One row per trailer, except that a run of trailers sharing a key becomes a
+ * single row listing every value. A line git kept in the block without being
+ * able to split it, such as a cherry-pick note, gets a row of its own.
+ */
+static void print_trailers(const char *msg)
+{
+ struct trailer_iterator iter;
+ struct strbuf key = STRBUF_INIT;
+ int open = 0;
+
+ html("<table class='commit-trailers'>\n");
+ trailer_iterator_init(&iter, msg);
+ while (trailer_iterator_advance(&iter)) {
+ if (!iter.val.len) {
+ if (open)
+ html("</td></tr>\n");
+ open = 0;
+ html("<tr><td colspan='2'>");
+ print_filtered_text(iter.key.buf);
+ html("</td></tr>\n");
+ continue;
+ }
+ if (open && !strcasecmp(key.buf, iter.key.buf))
+ html("<br>");
+ else {
+ if (open)
+ html("</td></tr>\n");
+ strbuf_reset(&key);
+ strbuf_addbuf(&key, &iter.key);
+ html("<tr><th>");
+ html_txt(key.buf);
+ html("</th><td>");
+ open = 1;
+ }
+ print_trailer_value(key.buf, iter.val.buf);
+ }
+ if (open)
+ html("</td></tr>\n");
+ html("</table>\n");
+ trailer_iterator_release(&iter);
+ strbuf_release(&key);
+}
+
+/*
+ * Git finds the trailer block by looking at the whole message, title included,
+ * so the subject goes back in front of the body before asking, and the offsets
+ * that come back are shifted by that prefix. The blank line that separated the
+ * body from its block is dropped along with the block.
+ */
+static void print_message(struct commitinfo *info)
+{
+ struct strbuf full = STRBUF_INIT;
+ struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;
+ struct trailer_block *block;
+ LIST_HEAD(items);
+ size_t prefix, start, end;
+ char *body = NULL;
+
+ if (ctx.repo->enable_trailers) {
+ strbuf_addf(&full, "%s\n\n%s", info->subject, info->msg);
+ prefix = full.len - strlen(info->msg);
+ opts.no_divider = 1;
+ block = parse_trailers(&opts, full.buf, &items);
+ start = trailer_block_start(block);
+ end = trailer_block_end(block);
+ free_trailers(&items);
+ trailer_block_release(block);
+ if (start >= prefix && start < end) {
+ size_t len = start - prefix;
+
+ while (len > 0 && info->msg[len - 1] == '\n')
+ len--;
+ body = xmemdupz(info->msg, len);
+ }
+ }
+
+ html("<div class='commit-msg'>");
+ print_filtered_text(body ? body : info->msg);
+ html("</div>\n");
+ if (body)
+ print_trailers(full.buf);
+
+ free(body);
+ strbuf_release(&full);
+}
+
void cgit_print_commit(char *hex, const char *prefix)
{
struct commit *commit;
@@ -145,9 +269,7 @@ void cgit_print_commit(char *hex, const char *prefix)
print_filtered_text(info->subject);
cgit_print_commit_decorations(commit);
html("</div>\n");
- html("<div class='commit-msg'>");
- print_filtered_text(info->msg);
- html("</div>\n");
+ print_message(info);
if (notes.len != 0) {
html("<div class='notes-header'>Notes</div>\n");
html("<div class='notes'>");
diff --git a/tests/t0206-trailers.sh b/tests/t0206-trailers.sh
new file mode 100755
index 0000000..fe96c5a
--- /dev/null
+++ b/tests/t0206-trailers.sh
@@ -0,0 +1,91 @@
+#!/bin/sh
+
+# The trailer table on the commit page. A commit whose message ends in a
+# trailer block is rendered with enable-trailers on, through the exec dump
+# filter as both trailer filter and email filter so the arguments each one
+# receives show in the page, then with the flag off globally and off for the
+# one repository. A commit with no block is checked to get no table.
+
+test_description='Check the commit trailers table'
+CGIT_TEST_NO_CREATE_REPOS=YesPlease
+. ./setup.sh
+
+test_expect_success 'create a repository with a trailer-shaped message' '
+ test_create_repo repos/trailers &&
+ (
+ cd repos/trailers &&
+ echo content >file &&
+ git add file &&
+ git commit -m "plain commit" &&
+ echo more >>file &&
+ git add file &&
+ git commit -F - <<-\EOF
+ Fix the frobnicator
+
+ The body keeps its colon: it is not a trailer.
+
+ Fixes: 1234567 ("plain commit")
+ Signed-off-by: A U Thor <author@example.com>
+ Signed-off-by: C O Mitter <committer@example.com>
+ (cherry picked from commit deadbeefcafe1234)
+ Link: https://example.com/?a=1&b=2
+ EOF
+ )
+'
+
+test_expect_success 'point cgit at it with trailers on and the dump filters' '
+ cat >cgitrc <<-EOF &&
+ virtual-root=/
+ cache-size=0
+ enable-trailers=1
+ enable-filter-overrides=1
+ repo.url=trailers
+ repo.path=$PWD/repos/trailers/.git
+ repo.trailer-filter=exec:$FILTER_DIRECTORY/dump.sh
+ repo.email-filter=exec:$FILTER_DIRECTORY/dump.sh
+ EOF
+ cgit_url "trailers/commit/" >tmp
+'
+
+test_expect_success 'the body ends where the block begins' '
+ grep "<div class=.commit-msg.>The body keeps its colon: it is not a trailer.</div>" tmp
+'
+
+test_expect_success 'a value reaches the trailer filter with its key and page' '
+ grep "<tr><th>Fixes</th><td>Fixes commit 1234567 (.PLAIN COMMIT.)</td></tr>" tmp
+'
+
+test_expect_success 'a run of one key collapses into one row through the email filter' '
+ grep "<tr><th>Signed-off-by</th><td><author@example.com> commit A U THOR &LT;AUTHOR@EXAMPLE.COM&GT;<br><committer@example.com> commit C O MITTER &LT;COMMITTER@EXAMPLE.COM&GT;</td></tr>" tmp
+'
+
+test_expect_success 'a line git could not split gets a row of its own' '
+ grep "<tr><td colspan=.2.>(cherry picked from commit deadbeefcafe1234)</td></tr>" tmp
+'
+
+test_expect_success 'the escaped value is what the filter sees' '
+ grep "<tr><th>Link</th><td>Link commit HTTPS://EXAMPLE.COM/?A=1&AMP;B=2</td></tr>" tmp
+'
+
+test_expect_success 'a message with no block gets no table' '
+ plain=$(git -C repos/trailers rev-parse HEAD~1) &&
+ cgit_url "trailers/commit/&id=$plain" >tmp &&
+ grep "<div class=.commit-msg.></div>" tmp &&
+ ! grep "commit-trailers" tmp
+'
+
+test_expect_success 'with the flag off the message stays whole' '
+ sed "s/^enable-trailers=1/enable-trailers=0/" cgitrc >cgitrc-off &&
+ CGIT_CONFIG="$PWD/cgitrc-off" QUERY_STRING="url=trailers/commit/" cgit >tmp &&
+ ! grep "commit-trailers" tmp &&
+ grep "Signed-off-by: A U Thor" tmp
+'
+
+test_expect_success 'the repository flag overrides the global one' '
+ echo "repo.enable-trailers=0" >>cgitrc &&
+ cgit_url "trailers/commit/" >tmp &&
+ ! grep "commit-trailers" tmp &&
+ grep "Signed-off-by: A U Thor" tmp
+'
+
+test_done