/*
 * fhist - file history and comparison tools
 * Copyright (C) 1991-1994, 1998-2000, 2002, 2006, 2008, 2010, 2012 Peter Miller
 *
 * Derived from a work
 * Copyright (C) 1990 David I. Bell.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <common/ac/ctype.h>
#include <common/ac/errno.h>
#include <common/ac/pwd.h>
#include <common/ac/stdio.h>
#include <common/ac/string.h>
#include <common/ac/time.h>
#include <common/ac/unistd.h>
#include <libexplain/fclose.h>
#include <libexplain/fflush.h>
#include <libexplain/fopen.h>
#include <libexplain/output.h>
#include <libexplain/strdup.h>

#include <common/cmalloc.h>
#include <common/compare.h>
#include <common/error_intl.h>
#include <common/fcheck.h>
#include <common/fileio.h>
#include <common/input/file.h>
#include <common/input/file_text.h>
#include <common/input/quotprinenco.h>
#include <common/str.h>
#include <common/trace.h>
#include <common/undo.h>

#include <fhist/breaks.h>
#include <fhist/fhist.h>
#include <fhist/modlin.h>
#include <fhist/subroutine.h>
#include <fhist/update.h>


static char *
get_user_login_name(void)
{
    static char     buffer[20];
    struct passwd   *pw;
    int             uid;

    uid = geteuid();
    pw = getpwuid(uid);
    if (!pw)
        sprintf(buffer, "%d", uid);
    else
    {
        size_t          len;

        len = strlen(pw->pw_name);
        if (len >= sizeof(buffer))
            len = sizeof(buffer) - 1;
        memcpy(buffer, pw->pw_name, len);
        buffer[len] = 0;
    }
    return buffer;
}


/*
 * Add the lines in the history file for the specified new edit number.
 * This are the beginning and end lines, the file line, and the default
 * and user-supplied remark lines.  These lines are written to the current
 * position in the file.  Errors are remembered by the file structure.
 */

static void
addhistory(FILE *fp, const char *remarks, long editnumber)
{
    time_t          bintime;    /* binary time of day */
    int             column;

    time(&bintime);
    fprintf(fp, "%c %ld\n", T_BEGINEDIT, editnumber);
    fprintf(fp, "%c %s   %s", T_REMARK, get_user_login_name(), ctime(&bintime));
    column = 0;
    while (*remarks)
    {
        unsigned char c = *remarks++;
        if (c == 0)
            break;
        if (column == 0)
            fprintf(fp, "%c ", T_REMARK);
        if (c == '\n')
            column = 0;
        else
            column++;
        putc(c, fp);
    }
    if (column > 0)
        putc('\n', fp);
    fprintf(fp, "%c 0\n", T_FILE);
    fprintf(fp, "%c %ld 0 0\n", T_ENDEDIT, editnumber);
}


/*
 * Describe help about entering remarks.
 */

static void
helpremarks(void)
{
    sub_context_ty  *scp;
    string_ty       *s;

    scp = sub_context_new();
    sub_var_set_charstar(scp, "Module", sc.modulename);
    sub_var_optional(scp, "Module");
    s = subst_intl(scp, i18n("remarks help here\n"));
    sub_context_delete(scp);
    printf("\n%s\n", s->str_text);
    str_free(s);
}


/*
 * Read remark lines from the user to describe the reason why an edit
 * was made.  This is made up of lines separated by nulls, with a double
 * null at the end.  The remarks can be from the terminal or from a file.
 *
 * Remark files are text.
 */

static const char *
getremarks(const char *modulename, const char *remarkname, long editnumber)
{
    char            *buf;       /* buffer for input */
    FILE            *fp;        /* remark file */
    char            *cp;        /* current line for retyping */
    long            avail;      /* total characters available */
    long            used;       /* number of characters used */
    long            line;
    int             ch;
    sub_context_ty  *scp;
    string_ty       *s;

    if (remarkname && (*remarkname == '\0'))    /* no remarks at all */
        return "";
    used = 0;
    avail = 400;
    buf = cm_alloc_and_check(avail + 4);

    /*
     * If remark file name was supplied, read remarks from it.
     */
    if (remarkname)
    {
        fp = explain_fopen_or_die(remarkname, "r");
        for (;;)
        {
            int bin = 0;
            cp = readlinef(fp, &line, 0, remarkname, &bin);
            if (!cp)
                break;
            if (bin)
                binary_fatal(remarkname);
            if ((used + line) >= avail)
            {
                avail = used + line + 200;
                buf = cm_realloc_and_check(buf, avail + 4);
            }
            strcpy(&buf[used], cp);
            used += (line + 1);
        }
        explain_fclose_or_die(fp);
        fp = 0;
        buf[used] = '\0';
        return buf;
    }

    /*
     * No remark file specified, so get remarks from user.
     */
    scp = sub_context_new();
    sub_var_set_charstar(scp, "Module", modulename);
    sub_var_set_long(scp, "Number", editnumber);
    s =
        subst_intl
        (
            scp,
i18n("please type remarks for edit $number of \"$module\" (type \"!h\" \
for help)")
        );
    sub_context_delete(scp);
    printf("\n%s\n", s->str_text);
    str_free(s);
    line = 1;
    for (;;)
    {
        if ((used + 200) > avail)
        {
            avail += 200;
            buf = cm_realloc_and_check(buf, avail + 4);
        }
        printf("%ld> ", line);
        fflush(stdout);
        if (fgets(&buf[used], 200, stdin) == NULL)
            break;
        ch = buf[used];
        if (ch == '\n')
                break;
        if (ch != '!')
        {
            /* normal line */
            used += strlen(&buf[used]) + 1;
            line++;
            continue;
        }
        buf[used] = '\0';
        ch = buf[used+1];               /* control line */
        if (isupper((unsigned char)ch))
            ch = tolower((unsigned char)ch);
        switch (ch)
        {
        case 'b':
            /* back up to previous line */
            if (line <= 1)
                break;
            line--;
            cp = buf;
            for (ch = 1; ch < line; ch++)
                cp += (strlen(cp) + 1);
            used = (cp - buf);
            break;

        case 'c':
            /* restart the remark */
            scp = sub_context_new();
            s =
                subst_intl
                (
                    scp,
                    i18n("remarks cleared, please type new remarks:")
                );
            sub_context_delete(scp);
            printf("\n%s\n", s->str_text);
            str_free(s);
            used = 0;
            line = 1;
            break;

        case 'h':
            /* help */
            helpremarks();
            break;

        case 'q':
            /* quit the command */
            fatal_intl(0, i18n("no action taken"));

        case 'r':
            /* retype remarks */
            printf("\n");
            for (ch = 0, cp = buf; *cp; cp += (strlen(cp) + 1))
                printf("%d> %s", ++ch, cp);
            break;

        default:
            /* unknown command */
            if (!isgraph((unsigned char)ch))
                ch = ' ';
            scp = sub_context_new();
            sub_var_set_format(scp, "Name", "!%c", ch);
            s = subst_intl(scp, i18n("unknown command \"$name\" - ignored"));
            sub_context_delete(scp);
            printf("%s\n", s->str_text);
            str_free(s);
            break;
        }
    }
    buf[used] = '\0';           /* double null to end string */
    return buf;
}


/*
 * Do the copy of the source file to the destination file, while
 * modifying the lines as necessary.  Returns nonzero on an error.
 */

static void
docopy(input_ty *ip, FILE *op)
{
    char            *cp;        /* current line */
    long            linelen;    /* current line length */
    long            i;          /* line count */

    for (i = sc.modifylines; i > 0; i--)
    {
        int bin = 0;
        cp = input_readline(ip, &linelen, 0, &bin);
        if (cp == NULL)
            return;
        if (bin)
            binary_fatal(input_name(ip));
        cp = modifyline(cp, &linelen, (INFO *) NULL);
        writefx(op, cp, linelen);
    }
    for (;;)
    {
        int bin = 0;
        cp = input_readline(ip, &linelen, 0, &bin);
        if (cp == NULL)
            return;
        if (bin)
            binary_fatal(input_name(ip));
        writefx(op, cp, linelen);
    }
}


/*
 * Create a new module.
 * This creates the new history file, and the initial source file.
 * This will start as edit 1, with the given file as the original source.
 *
 * The history (.e) file is binary, because we need to seek in it.
 * The source (.s) file is text, because we don't need to seek in it.
 * The input files are text, by definition.
 * The output files are text, by definition.
 */

void
createhistory(const char *inputname, const char *editname)
{
    FILE            *fp;        /* history file */
    input_ty        *ip;        /* input filename */
    FILE            *up;        /* updated source file */
    long            pospos, editpos;
    const char      *remarks;   /* remarks for new file */
    sub_context_ty  *scp;

    /*
     * Verify that the module doesn't already exist, and
     * open up the initial file for reading.
     */
    fp = fopen(sc.historyname, "rb");
    if (fp)
    {
        fclose(fp);
        fp = 0;
        scp = sub_context_new();
        sub_var_set_charstar(scp, "Module", sc.modulename);
        sub_var_set_charstar(scp, "File_Name", sc.basename);
        fatal_intl
        (
            scp,
            i18n("module \"$module\" already exists as \"$filename\"")
        );
    }
    if (errno != ENOENT)
    {
        explain_output_error_and_die("%s", explain_fopen(sc.historyname, "rb"));
        /* NOTREACHED */
    }
    up = fopen(sc.sourcename, "rb");
    if (up)
    {
        fclose(up);
        up = 0;
        scp = sub_context_new();
        sub_var_set_charstar(scp, "Module", sc.modulename);
        sub_var_set_charstar(scp, "File_Name", sc.basename);
        fatal_intl
        (
            scp,
           i18n("source for module \"$module\" already exists as \"$filename\"")
        );
    }
    if (errno != ENOENT)
    {
        explain_output_error_and_die("%s", explain_fopen(sc.sourcename, "rb"));
    }

    if (fc.binary)
    {
        ip = input_file_open(inputname);
        ip = input_quoted_printable_encode(ip, 1);
    }
    else
        ip = input_file_text_open(inputname);

    /*
     * Try to create the history source file, and if successful, then
     * get the initial remarks for the module.
     */
    if (fc.verbosity)
    {
        scp = sub_context_new();
        sub_var_set_charstar(scp, "Module", sc.modulename);
        sub_var_set_charstar(scp, "File_Name", inputname);
        error_intl
        (
            scp,
         i18n("creating new module \"$module\" from initial file \"$filename\"")
        );
        sub_context_delete(scp);
    }
    undo_unlink(sc.sourcename);
    up = explain_fopen_or_die(sc.sourcename, "wb");
    if (sc.remark_string)
        remarks = sc.remark_string;
    else
        remarks = getremarks(sc.modulename, sc.remarkname, 1L);

    /*
     * Copy the original file into the history latest source file,
     * with the extra line at the front identifying it as edit 1.
     */
    breaksoff();
#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
    {
        explain_output_error
        (
            "[Copying \"%s\" to \"%s\"]",
            inputname,
            sc.sourcename
        );
    }
#endif
    fprintf(up, "%c 1\n", T_BEGINEDIT);
    docopy(ip, up);
    explain_fclose_or_die(up);
    up = 0;
    input_delete(ip);

    /*
     * Write the initial history file data
     *  create it binary
     */
#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
        explain_output_error("[Creating history file \"%s\"]", sc.historyname);
#endif
    undo_unlink(sc.historyname);
    fp = explain_fopen_or_die(sc.historyname, "w+b");
    fprintf(fp, HEADERFORMAT, T_HEADER, 1L, 1L, 0L, 0L);
    editpos = ftell(fp);
    addhistory(fp, remarks, 1L);
    pospos = ftell(fp);
    fprintf(fp, (editname ? "%c 1 %ld %s\n" : "%c 1 %ld\n"),
            T_POSITION, editpos, editname);
    fprintf(fp, "%c %ld\n", T_EOF, ftell(fp));
    seekf(fp, 0L);
    fprintf(fp, HEADERFORMAT, T_HEADER, 1L, 1L, 0L, pospos);
    explain_fclose_or_die(fp);
    fp = 0;
#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
        explain_output_error("[Creation complete]");
#endif
    breakson();
}


/*
 * Write the new source file into a temporary file in the proper directory.
 * This is essentially a copy, except for the initial line indicating the
 * edit number of the new source.  This copy is done to guarantee that there
 * is enough disk space in order to finish the update.  If there is not, then
 * nothing harmful will have been done to the old source or history files.
 * It is assumed that the history file has already been opened.
 *
 * The history (.e) file is binary, because we need to seek in it.
 * The source (.s) file is text, because we don't need to seek in it.
 * The input files are text, by definition.
 * The output files are text, by definition.
 */

static void
writesource(const char *inputname)
{
    input_ty        *ip;                /* input file */
    FILE            *up;                /* output file */
    string_ty       *update_name;

    update_name = str_format("%s%s", sc.basename, EXT_NEW);
#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
    {
        explain_output_error
        (
            "[Copying \"%s\" to temporary file \"%s\"]",
            inputname,
            updatename->str_text
        );
    }
#endif

    if (fc.binary)
    {
        ip = input_file_open(inputname);
        ip = input_quoted_printable_encode(ip, 1);
    }
    else
        ip = input_file_text_open(inputname);

    undo_unlink(update_name->str_text);
    up = explain_fopen_or_die(update_name->str_text, "wb");
    fprintf(up, "%c %ld\n", T_BEGINEDIT, sc.lastedit + 1);
    docopy(ip, up);
    explain_fclose_or_die(up);
    up = 0;
    input_delete(ip);
    str_free(update_name);
}


/*
 * Compare two files, and return the changes into a snake list.
 * This works by calling the fcomp module which is loaded with us.
 * Does not return if an error ocurred.
 */

static void
comparefiles(const char *file1, const char *file2, long skip2)
{
    trace(("comparefiles(file1 = \"%s\", file2 = \"%s\", slip2 = %d)\n{\n",
        file1, file2, skip2));
#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
    {
        explain_output_error
        (
            "[Comparing files \"%s\" and \"%s\"]",
            file1,
            file2
        );
    }
#endif
    fc.fileBskip = skip2;
    fc.fileAmodify = sc.modifylines;
    fc.fileBmodify = sc.modifylines;
    fc.maxchanges = INFINITY;
    fcomp(file1, file2);
    trace(("}\n"));
}


/*
 * Swap the snake line numbers for two files to appear as if the comparison
 * was in the other order.  This is used when showing differences and then
 * updating, since showing differences needs the changes to the new version,
 * but updating needs the changes back to the older version.
 */

static void
swapfiles(void)
{
    SNAKE           *sp;        /* current snake */
    long            temp;       /* temporary for swapping */
    FILEINFO        tempfile;   /* temporary for swapping */

    for (sp = fc.snakelist; sp; sp = sp->next)
    {
        temp = sp->line1;
        sp->line1 = sp->line2;
        sp->line2 = temp;
    }
    tempfile = fc.fileA;
    fc.fileA = fc.fileB;
    fc.fileB = tempfile;
    temp = fc.deletes;
    fc.deletes = fc.inserts;
    fc.inserts = temp;
}


/*
 * Write out the edit script corresponding to the results of the
 * comparison between two files.  Errors cause premature finishing,
 * and are accessible by using ferror afterwards.  This routine knows
 * the data structures defined by the fcomp module.  This routine is
 * also called after the begin line and remark lines have been
 * skipped over.
 */

static void
writeedits(FILE *fp, long editnumber)
{
    SNAKE           *sp;        /* current snake */
    long            line1;
    long            line2;      /* current line numbers in files */
    long            inserts;
    long            deletes;    /* number of inserts and deletes needed */
    long            totalinserts;
    long            totaldeletes; /* total lines inserted or deleted */
    long            i;
    char            *cp;        /* line being inserted */

    totalinserts = 0;
    totaldeletes = 0;
    line1 = 0;
    line2 = 0;
    for (sp = fc.snakelist; sp; sp = sp->next)
    {
        deletes = sp->line1 - line1;
        inserts = sp->line2 - line2;
#if 0
        if (fc.debugflag)
        {
                explain_output_error
                (
            "[line1 %ld  line2 %ld  count %ld  insert %ld  delete %ld]",
                        sp->line1,
                        sp->line2,
                        sp->count,
                        inserts,
                        deletes
                );
}
#endif
        if (deletes || inserts)
        {
            fprintf
            (
                fp,
                "%c %ld %ld %ld\n",
                T_CHANGE,
                line1 + 1,
                inserts,
                deletes
            );
        }
        for (i = 0; i < inserts; i++)
        {
            fprintf(fp, "%c ", T_TEXT);
            cp = fc.fileB.f_lines[line2 + i]->l_data;
            writefx(fp, cp, (long) strlen(cp));
        }
        totalinserts += inserts;
        totaldeletes += deletes;
        line1 = sp->line1 + sp->count;
        line2 = sp->line2 + sp->count;
    }
    fprintf(fp, "%c %ld %ld %ld\n", T_ENDEDIT,
            editnumber, totalinserts, totaldeletes);
    explain_fflush_or_die(fp);
}


/*
 * Update the history file by appending the changes given by a file.
 */

void
updatehistory(const char *inputname, const char *editname)
{
    FILE            *fp;        /* history file */
    char            *cp;        /* current line of data */
    POS             *pp;        /* current entry in position table */
    POS             *postable;  /* position table */
    const char      *remarks;   /* remarks */
    long            newtablepos; /* position of new position table */
    long            lasteditpos; /* position of beginning of last edit */
    long            appendpos;  /* position to append new changes to */
    long            tempedit;   /* edit number just scanned from line */
    long            newedit;    /* number of new edit */
    long            edit;       /* current edit number */
    sub_context_ty  *scp;

    /*
     * Make sure that the input file contains the right version number
     * before beginning the update, and if so, then get the remarks
     * for the edit.
     */
    trace(("updatehistory()\n{\n"));
    fp = openhistoryfile(OHF_WRITE);
    if (!sc.forceupdateflag)
    {
        tempedit = findedit(inputname, fc.binary);
        if ((tempedit > 0) && (tempedit != sc.lastedit))
        {
            scp = sub_context_new();
            sub_var_set_charstar(scp, "Module", sc.modulename);
            sub_var_set_long(scp, "Number1", tempedit);
            sub_var_set_long(scp, "Number1", sc.lastedit);
            fatal_with_filename
            (
                inputname,
                scp,
                i18n("edit $number1 instead of $number2 for module \"$module\"")
            );
        }
    }
    newedit = sc.lastedit + 1;
    if (fc.verbosity)
    {
        scp = sub_context_new();
        sub_var_set_charstar(scp, "File_Name", inputname);
        sub_var_set_charstar(scp, "Module", sc.modulename);
        sub_var_set_long(scp, "Number", newedit);
        error_intl
        (
            scp,
          i18n("installing \"$filename\" as edit $number of module \"$module\"")
        );
        sub_context_delete(scp);
    }
    if (sc.remark_string)
        remarks = sc.remark_string;
    else
        remarks = getremarks(sc.modulename, sc.remarkname, newedit);

    /*
     * See if we are doing a straight update, or an update after a
     * display of the differences.  If the latter, then we already have
     * done the comparison, but we need to swap the differences.
     */
    if (fc.snakelist)
        swapfiles();
    else
        comparefiles(inputname, sc.sourcename, 1L);
    postable = readpostable(fp);

    /*
     * Now go back and verify that the last edit actually exists where it
     * is supposed to be.  We will overwrite it with the changes.
     * Skip over all remark lines to position to the place to append
     * the new changes.  This will be at the location of the file line
     * for the latest edit.
     */
    lasteditpos = postable[1].p_pos;
    cp = get_a_line(fp, lasteditpos, T_BEGINEDIT, sc.historyname);
    cp = getnumber(cp, &tempedit);
    if ((cp == NULL) || (tempedit != sc.lastedit))
    {
        fatal_with_filename
        (
            sc.historyname,
            0,
            i18n("bad begin edit line for latest edit")
        );
    }

    for (;;)
    {
        int bin = 0;
        appendpos = ftell(fp);
        cp = readlinef(fp, (long *) NULL, 0, sc.historyname, &bin);
        if (cp == NULL)
        {
            fatal_with_filename
            (
                sc.historyname,
                0,
                i18n("error reading remark lines for latest edit")
            );
        }
        if (bin)
            binary_fatal(sc.historyname);
        if (*cp == T_FILE)      /* the file line, all done */
            break;
        if (*cp != T_REMARK)    /* error if not remark line */
        {
            fatal_with_filename
            (
                sc.historyname,
                0,
                i18n("unexpected line type in latest edit")
            );
        }
    }

    /*
     * Now write the new version of the source file under a temporary name.
     * This is to prevent problems due to quota errors.  So if this fails
     * we have done nothing bad.
     */
    breaksoff();
    writesource(inputname);

    /*
     * Now position back to the file line in the latest edit, and
     * overwrite it with the changes for the new edit.  Errors will
     * be checked for later and handled there, so we can recover from
     * disk full or quota exceeded problems.
     */
#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
        explain_output_error("[Appending edits to history file]");
#endif
    seekf(fp, appendpos);
    writeedits(fp, sc.lastedit);

    /*
     * Now append the latest edit template to the file, and follow
     * that with the saved position table, and then append a new
     * end of file line.  a new end of file line.  Save the
     * position of the beginning of the table for updating the
     * header.  Errors are checked for later.
     */
    pp = postable;
    pp->p_pos = ftell(fp);
    pp->p_names = editname ? explain_strdup_or_die(editname) : 0;
    addhistory(fp, remarks, newedit);
    newtablepos = ftell(fp);
    for (edit = newedit; edit >= sc.firstedit; edit--, pp++)
    {
        fprintf
        (
            fp,
            (pp->p_names ? "%c %ld %ld %s\n" : "%c %ld %ld\n"),
            T_POSITION,
            edit,
            pp->p_pos,
            pp->p_names
        );
    }
    fprintf(fp, "%c %ld\n", T_EOF, ftell(fp));

    /*
     * Force the last bit of the output to the file, then check for
     * errors.  If an error occurred during any of the above writes
     * then we will try to recover by rewriting the old position
     * table back to where it was.  This should succeed since quota
     * exceeded or disk full errors cannot occur since the position
     * table was there previously!
     */
    explain_fflush_or_die(fp);
    if (renamefiles(EXT_SOURCE))
    {
        errno = 0;
        seekf(fp, appendpos);
        fprintf(fp, "%c 0\n", T_FILE);
        fprintf(fp, "%c %ld 0 0\n", T_ENDEDIT, sc.lastedit);
        if (ftell(fp) != sc.tablepos)
        {
            fatal_with_filename
            (
                sc.historyname,
                0,
                i18n("position table moved")
            );
        }
        pp = &postable[1];
        for (edit = sc.lastedit; edit >= sc.firstedit; edit--, pp++)
        {
            fprintf
            (
                fp,
                (pp->p_names ? "%c %ld %ld %s\n" : "%c %ld %ld\n"),
                T_POSITION,
                edit,
                pp->p_pos,
                pp->p_names
            );
        }
        fprintf(fp, "%c %ld\n", T_EOF, ftell(fp));
        explain_fclose_or_die(fp);
        fp = 0;

        scp = sub_context_new();
        sub_var_set_charstar(scp, "File_Name", sc.historyname);
        fatal_intl(scp, i18n("file \"$filename\" is not damaged"));
    }

    /*
     * As the last step for inserting the new edit, rewrite the old header
     * line to update the new last edit number and the new position of the
     * beginning of the position table.  This line must be of fixed length.
     * This write should succeed since the file is not being grown in size.
     */
    seekf(fp, 0L);
    fprintf(fp, HEADERFORMAT, T_HEADER, sc.firstedit, newedit, 0L, newtablepos);
    explain_fclose_or_die(fp);
    fp = 0;
    if (fc.verbosity)
    {
        scp = sub_context_new();
        sub_var_set_long(scp, "Number", newedit);
        sub_var_set_charstar(scp, "Module", sc.modulename);
        sub_var_set_long(scp, "Inserts", fc.deletes); /* deliberate */
        sub_var_set_long(scp, "Deletes", fc.inserts); /* deliberate */
        sub_var_set_long(scp, "MAtches", fc.matches);
        error_intl
        (
            scp,
            i18n
            (
                "edit $number of module \"$module\" saved (inserts $inserts, "
                "deletes $deletes, matches $matches)"
            )
        );
        sub_context_delete(scp);
    }
    fflush(stdout);
    breakson();
    trace(("}\n"));
}


/* vim: set ts=8 sw=4 et : */
