root/usr.bin/cvs/commit.c
/*      $OpenBSD: commit.c,v 1.160 2019/06/28 13:35:00 deraadt Exp $    */
/*
 * Copyright (c) 2006 Joris Vink <joris@openbsd.org>
 * Copyright (c) 2006 Xavier Santolaria <xsa@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/stat.h>

#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "cvs.h"
#include "diff.h"
#include "remote.h"

void                     cvs_commit_local(struct cvs_file *);
void                     cvs_commit_check_files(struct cvs_file *);
void                     cvs_commit_loginfo(char *);
void                     cvs_commit_lock_dirs(struct cvs_file *);

static BUF *commit_diff(struct cvs_file *, RCSNUM *, int);
static void commit_desc_set(struct cvs_file *);

struct  file_info_list   files_info;
struct  trigger_list    *line_list;

struct cvs_flisthead    files_affected;
struct cvs_flisthead    files_added;
struct cvs_flisthead    files_removed;
struct cvs_flisthead    files_modified;

char    *logmsg = NULL;
char    *loginfo = NULL;

static int      conflicts_found;

struct cvs_cmd cvs_cmd_commit = {
        CVS_OP_COMMIT, CVS_USE_WDIR, "commit",
        { "ci", "com" },
        "Check files into the repository",
        "[-flR] [-F logfile | -m msg] [-r rev] ...",
        "F:flm:Rr:",
        NULL,
        cvs_commit
};

int
cvs_commit(int argc, char **argv)
{
        int flags;
        int ch, Fflag, mflag;
        struct module_checkout *mc;
        struct cvs_recursion cr;
        struct cvs_filelist *l;
        struct file_info *fi;
        char *arg = ".", repo[PATH_MAX];

        flags = CR_RECURSE_DIRS;
        Fflag = mflag = 0;

        while ((ch = getopt(argc, argv, cvs_cmd_commit.cmd_opts)) != -1) {
                switch (ch) {
                case 'F':
                        /* free previously assigned value */
                        free(logmsg);
                        logmsg = cvs_logmsg_read(optarg);
                        Fflag = 1;
                        break;
                case 'f':
                        break;
                case 'l':
                        flags &= ~CR_RECURSE_DIRS;
                        break;
                case 'm':
                        /* free previously assigned value */
                        free(logmsg);
                        logmsg = xstrdup(optarg);
                        mflag = 1;
                        break;
                case 'R':
                        flags |= CR_RECURSE_DIRS;
                        break;
                case 'r':
                        break;
                default:
                        fatal("%s", cvs_cmd_commit.cmd_synopsis);
                }
        }

        argc -= optind;
        argv += optind;

        /* -F and -m are mutually exclusive */
        if (Fflag && mflag)
                fatal("cannot specify both a log file and a message");

        RB_INIT(&files_affected);
        RB_INIT(&files_added);
        RB_INIT(&files_removed);
        RB_INIT(&files_modified);

        TAILQ_INIT(&files_info);
        conflicts_found = 0;

        cr.enterdir = NULL;
        cr.leavedir = NULL;
        cr.fileproc = cvs_commit_check_files;
        cr.flags = flags;

        if (argc > 0)
                cvs_file_run(argc, argv, &cr);
        else
                cvs_file_run(1, &arg, &cr);

        if (conflicts_found != 0)
                fatal("%d conflicts found, please correct these first",
                    conflicts_found);

        if (RB_EMPTY(&files_affected))
                return (0);

        if (cvsroot_is_remote()) {
                if (logmsg == NULL) {
                        logmsg = cvs_logmsg_create(NULL, &files_added,
                            &files_removed, &files_modified);
                        if (logmsg == NULL)
                                fatal("This shouldnt happen, honestly!");
                }
                cvs_client_connect_to_server();
                cr.fileproc = cvs_client_sendfile;

                if (argc > 0)
                        cvs_file_run(argc, argv, &cr);
                else
                        cvs_file_run(1, &arg, &cr);

                if (!(flags & CR_RECURSE_DIRS))
                        cvs_client_send_request("Argument -l");

                cvs_client_send_logmsg(logmsg);
                cvs_client_send_files(argv, argc);
                cvs_client_senddir(".");
                cvs_client_send_request("ci");
                cvs_client_get_responses();
        } else {
                cvs_get_repository_name(".", repo, PATH_MAX);

                line_list = cvs_trigger_getlines(CVS_PATH_COMMITINFO, repo);
                if (line_list != NULL) {
                        RB_FOREACH(l, cvs_flisthead, &files_affected) {
                                fi = xcalloc(1, sizeof(*fi));
                                fi->file_path = xstrdup(l->file_path);
                                TAILQ_INSERT_TAIL(&files_info, fi,
                                    flist);
                        }

                        if (cvs_trigger_handle(CVS_TRIGGER_COMMITINFO,
                            repo, NULL, line_list, &files_info)) {
                                cvs_log(LP_ERR,
                                    "Pre-commit check failed");
                                cvs_trigger_freelist(line_list);
                                goto end;
                        }

                        cvs_trigger_freelist(line_list);
                        cvs_trigger_freeinfo(&files_info);
                }

                if (cvs_server_active) {
                        if (logmsg == NULL)
                                fatal("no log message specified");
                } else if (logmsg == NULL) {
                        logmsg = cvs_logmsg_create(NULL, &files_added,
                            &files_removed, &files_modified);
                        if (logmsg == NULL)
                                fatal("This shouldnt happen, honestly!");
                }

                if (cvs_logmsg_verify(logmsg))
                        goto end;

                cr.fileproc = cvs_commit_lock_dirs;
                cvs_file_walklist(&files_affected, &cr);

                line_list = cvs_trigger_getlines(CVS_PATH_LOGINFO, repo);

                cr.fileproc = cvs_commit_local;
                cvs_file_walklist(&files_affected, &cr);

                if (line_list != NULL) {
                        cvs_commit_loginfo(repo);

                        cvs_trigger_handle(CVS_TRIGGER_LOGINFO, repo,
                            loginfo, line_list, &files_info);

                        free(loginfo);
                        cvs_trigger_freelist(line_list);
                        cvs_trigger_freeinfo(&files_info);
                }

                mc = cvs_module_lookup(repo);
                if (mc->mc_prog != NULL &&
                    (mc->mc_flags & MODULE_RUN_ON_COMMIT))
                        cvs_exec(mc->mc_prog, NULL, 0);
        }

end:
        cvs_trigger_freeinfo(&files_info);
        free(logmsg);
        return (0);
}

void
cvs_commit_loginfo(char *repo)
{
        BUF *buf;
        char pwd[PATH_MAX];
        struct cvs_filelist *cf;

        if (getcwd(pwd, sizeof(pwd)) == NULL)
                fatal("Can't get working directory");

        buf = buf_alloc(1024);

        cvs_trigger_loginfo_header(buf, repo);

        if (!RB_EMPTY(&files_added)) {
                buf_puts(buf, "Added Files:");

                RB_FOREACH(cf, cvs_flisthead, &files_added) {
                        buf_putc(buf, '\n');
                        buf_putc(buf, '\t');
                        buf_puts(buf, cf->file_path);
                }

                buf_putc(buf, '\n');
        }

        if (!RB_EMPTY(&files_modified)) {
                buf_puts(buf, "Modified Files:");

                RB_FOREACH(cf, cvs_flisthead, &files_modified) {
                        buf_putc(buf, '\n');
                        buf_putc(buf, '\t');
                        buf_puts(buf, cf->file_path);
                }

                buf_putc(buf, '\n');
        }

        if (!RB_EMPTY(&files_removed)) {
                buf_puts(buf, "Removed Files:");

                RB_FOREACH(cf, cvs_flisthead, &files_removed) {
                        buf_putc(buf, '\n');
                        buf_putc(buf, '\t');
                        buf_puts(buf, cf->file_path);
                }

                buf_putc(buf, '\n');
        }

        buf_puts(buf, "Log Message:\n");

        buf_puts(buf, logmsg);

        buf_putc(buf, '\n');
        buf_putc(buf, '\0');

        loginfo = buf_release(buf);
}

void
cvs_commit_lock_dirs(struct cvs_file *cf)
{
        char repo[PATH_MAX];

        cvs_get_repository_path(cf->file_wd, repo, sizeof(repo));
        cvs_log(LP_TRACE, "cvs_commit_lock_dirs: %s", repo);

        /* locks stay in place until we are fully done and exit */
        cvs_repository_lock(repo, 1);
}

void
cvs_commit_check_files(struct cvs_file *cf)
{
        char *tag;
        RCSNUM *branch, *brev;

        branch = brev = NULL;

        cvs_log(LP_TRACE, "cvs_commit_check_files(%s)", cf->file_path);

        if (cvsroot_is_remote())
                cvs_remote_classify_file(cf);
        else
                cvs_file_classify(cf, cvs_directory_tag);

        if (cf->file_type == CVS_DIR) {
                if (verbosity > 1)
                        cvs_log(LP_NOTICE, "Examining %s", cf->file_path);
                return;
        }

        if (cf->file_status == FILE_UPTODATE)
                return;

        if (cf->file_status == FILE_MERGE ||
            cf->file_status == FILE_PATCH ||
            cf->file_status == FILE_CHECKOUT ||
            cf->file_status == FILE_LOST ||
            cf->file_status == FILE_UNLINK) {
                cvs_log(LP_ERR, "conflict: %s is not up-to-date",
                    cf->file_path);
                conflicts_found++;
                return;
        }

        if (cf->file_status == FILE_CONFLICT &&
           cf->file_ent->ce_conflict != NULL) {
                cvs_log(LP_ERR, "conflict: unresolved conflicts in %s from "
                    "merging, please fix these first", cf->file_path);
                conflicts_found++;
                return;
        }

        if (cf->file_status == FILE_MODIFIED &&
            cf->file_ent->ce_conflict != NULL &&
            update_has_conflict_markers(cf)) {
                cvs_log(LP_ERR, "warning: file %s seems to still contain "
                    "conflict indicators", cf->file_path);
        }

        if (cf->file_ent != NULL && cf->file_ent->ce_date != -1) {
                cvs_log(LP_ERR, "conflict: cannot commit to sticky date for %s",
                    cf->file_path);
                conflicts_found++;
                return;
        }

        if (cvsroot_is_local()) {
                tag = cvs_directory_tag;
                if (cf->file_ent != NULL)
                        tag = cf->file_ent->ce_tag;

                if (tag != NULL && cf->file_rcs != NULL) {
                        brev = rcs_sym_getrev(cf->file_rcs, tag);
                        if (brev != NULL) {
                                if (!RCSNUM_ISBRANCH(brev)) {
                                        cvs_log(LP_ERR, "sticky tag %s is not "
                                            "a branch for file %s", tag,
                                            cf->file_path);
                                        conflicts_found++;
                                }
                        }
                }
        }

        free(branch);
        free(brev);

        if (cf->file_status != FILE_ADDED &&
            cf->file_status != FILE_REMOVED &&
            cf->file_status != FILE_MODIFIED)
                return;

        cvs_file_get(cf->file_path, 0, &files_affected, CVS_FILE);

        switch (cf->file_status) {
        case FILE_ADDED:
                cvs_file_get(cf->file_path, 0, &files_added, CVS_FILE);
                break;
        case FILE_REMOVED:
                cvs_file_get(cf->file_path, 0, &files_removed, CVS_FILE);
                break;
        case FILE_MODIFIED:
                cvs_file_get(cf->file_path, 0, &files_modified, CVS_FILE);
                break;
        }
}

void
cvs_commit_local(struct cvs_file *cf)
{
        char *tag;
        BUF *b, *d;
        int onbranch, isnew, histtype, branchadded;
        RCSNUM *nrev, *crev, *rrev, *brev;
        int openflags, rcsflags;
        char rbuf[CVS_REV_BUFSZ], nbuf[CVS_REV_BUFSZ];
        CVSENTRIES *entlist;
        char attic[PATH_MAX], repo[PATH_MAX], rcsfile[PATH_MAX];
        struct file_info *fi;

        cvs_log(LP_TRACE, "cvs_commit_local(%s)", cf->file_path);
        cvs_file_classify(cf, cvs_directory_tag);

        if (cvs_noexec == 1)
                return;

        if (cf->file_type != CVS_FILE)
                fatal("cvs_commit_local: '%s' is not a file", cf->file_path);

        tag = cvs_directory_tag;
        if (cf->file_ent != NULL && cf->file_ent->ce_tag != NULL)
                tag = cf->file_ent->ce_tag;

        branchadded = 0;
        switch (cf->file_status) {
        case FILE_ADDED:
                if (cf->file_rcs == NULL && tag != NULL) {
                        branchadded = 1;
                        cvs_add_tobranch(cf, tag);
                }
                break;
        case FILE_MODIFIED:
        case FILE_REMOVED:
                if (cf->file_rcs == NULL) {
                        cvs_log(LP_ERR, "RCS file for %s got lost",
                            cf->file_path);
                        return;
                }
                break;
        default:
                cvs_log(LP_ERR, "skipping bogus file `%s'", cf->file_path);
                return;
        }

        onbranch = 0;
        nrev = RCS_HEAD_REV;
        crev = NULL;
        rrev = NULL;
        d = NULL;

        if (cf->file_rcs != NULL && cf->file_rcs->rf_branch != NULL) {
                free(cf->file_rcs->rf_branch);
                cf->file_rcs->rf_branch = NULL;
        }

        if (cf->file_rcs != NULL) {
                rrev = rcs_head_get(cf->file_rcs);
                crev = rcs_head_get(cf->file_rcs);
                if (crev == NULL || rrev == NULL)
                        fatal("no head revision in RCS file for %s",
                            cf->file_path);

                if (tag != NULL) {
                        free(crev);
                        free(rrev);
                        brev = rcs_sym_getrev(cf->file_rcs, tag);
                        crev = rcs_translate_tag(tag, cf->file_rcs);
                        if (brev == NULL || crev == NULL) {
                                fatal("failed to resolve existing tag: %s",
                                    tag);
                        }

                        rrev = rcsnum_alloc();
                        rcsnum_cpy(brev, rrev, brev->rn_len - 1);

                        if (RCSNUM_ISBRANCHREV(crev) &&
                            rcsnum_cmp(crev, rrev, 0)) {
                                nrev = rcsnum_alloc();
                                rcsnum_cpy(crev, nrev, 0);
                                rcsnum_inc(nrev);
                        } else if (!RCSNUM_ISBRANCH(crev)) {
                                nrev = rcsnum_brtorev(brev);
                                if (nrev == NULL)
                                        fatal("failed to create branch rev");
                        } else {
                                fatal("this isnt suppose to happen, honestly");
                        }

                        free(brev);
                        free(rrev);
                        rrev = rcsnum_branch_root(nrev);

                        /* branch stuff was checked in cvs_commit_check_files */
                        onbranch = 1;
                }

                rcsnum_tostr(crev, rbuf, sizeof(rbuf));
        } else {
                strlcpy(rbuf, "Non-existent", sizeof(rbuf));
        }

        free(rrev);
        isnew = 0;
        if (cf->file_status == FILE_ADDED) {
                isnew = 1;
                rcsflags = RCS_CREATE;
                openflags = O_CREAT | O_RDONLY;
                if (cf->file_rcs != NULL) {
                        if (!onbranch) {
                                if (cf->in_attic == 0)
                                        cvs_log(LP_ERR, "warning: expected %s "
                                            "to be in the Attic",
                                            cf->file_path);

                                if (cf->file_rcs->rf_dead == 0)
                                        cvs_log(LP_ERR, "warning: expected %s "
                                            "to be dead", cf->file_path);

                                cvs_get_repository_path(cf->file_wd, repo,
                                    PATH_MAX);
                                (void)xsnprintf(rcsfile, PATH_MAX, "%s/%s%s",
                                    repo, cf->file_name, RCS_FILE_EXT);

                                if (rename(cf->file_rpath, rcsfile) == -1)
                                        fatal("cvs_commit_local: failed to "
                                            "move %s outside the Attic: %s",
                                            cf->file_path, strerror(errno));

                                free(cf->file_rpath);
                                cf->file_rpath = xstrdup(rcsfile);
                                isnew = 0;
                        }

                        rcsflags = RCS_READ | RCS_PARSE_FULLY;
                        openflags = O_RDONLY;
                        rcs_close(cf->file_rcs);
                }

                cf->repo_fd = open(cf->file_rpath, openflags);
                if (cf->repo_fd == -1)
                        fatal("cvs_commit_local: %s", strerror(errno));

                cf->file_rcs = rcs_open(cf->file_rpath, cf->repo_fd,
                    rcsflags, 0444);
                if (cf->file_rcs == NULL)
                        fatal("cvs_commit_local: failed to create RCS file "
                            "for %s", cf->file_path);

                commit_desc_set(cf);

                if (branchadded)
                        strlcpy(rbuf, "Non-existent", sizeof(rbuf));
        }

        if (verbosity > 1) {
                cvs_printf("Checking in %s:\n", cf->file_path);
                cvs_printf("%s <- %s\n", cf->file_rpath, cf->file_path);
                cvs_printf("old revision: %s; ", rbuf);
        }

        if (isnew == 0 && cf->file_rcs->rf_head == NULL)
                fatal("no head revision in RCS file for %s", cf->file_path);

        if (isnew == 0 && onbranch == 0)
                d = commit_diff(cf, cf->file_rcs->rf_head, 0);

        if (cf->file_status == FILE_REMOVED) {
                b = rcs_rev_getbuf(cf->file_rcs, crev, 0);
        } else if (onbranch == 1) {
                b = commit_diff(cf, crev, 1);
        } else {
                b = buf_load_fd(cf->fd);
        }

        if (isnew == 0 && onbranch == 0) {
                if (rcs_deltatext_set(cf->file_rcs, crev, d) == -1)
                        fatal("cvs_commit_local: failed to set delta");
        }

        if (rcs_rev_add(cf->file_rcs, nrev, logmsg, -1, NULL) == -1)
                fatal("cvs_commit_local: failed to add new revision");

        if (nrev == RCS_HEAD_REV)
                nrev = cf->file_rcs->rf_head;

        if (rcs_deltatext_set(cf->file_rcs, nrev, b) == -1)
                fatal("cvs_commit_local: failed to set new HEAD delta");

        if (cf->file_status == FILE_REMOVED) {
                if (rcs_state_set(cf->file_rcs, nrev, RCS_STATE_DEAD) == -1)
                        fatal("cvs_commit_local: failed to set state");
        }

        if (cf->file_status == FILE_ADDED && cf->file_ent->ce_opts != NULL) {
                int cf_kflag;

                cf_kflag = rcs_kflag_get(cf->file_ent->ce_opts + 2);
                rcs_kwexp_set(cf->file_rcs, cf_kflag);
        }

        rcs_write(cf->file_rcs);

        if (cf->file_status == FILE_REMOVED) {
                strlcpy(nbuf, "Removed", sizeof(nbuf));
        } else if (cf->file_status == FILE_ADDED) {
                if (cf->file_rcs->rf_dead == 1)
                        strlcpy(nbuf, "Initial Revision", sizeof(nbuf));
                else
                        rcsnum_tostr(nrev, nbuf, sizeof(nbuf));
        } else if (cf->file_status == FILE_MODIFIED) {
                rcsnum_tostr(nrev, nbuf, sizeof(nbuf));
        }

        if (verbosity > 1)
                cvs_printf("new revision: %s\n", nbuf);

        (void)unlink(cf->file_path);
        (void)close(cf->fd);
        cf->fd = -1;

        if (cf->file_status != FILE_REMOVED) {
                cvs_checkout_file(cf, nrev, NULL, CO_COMMIT);
        } else {
                entlist = cvs_ent_open(cf->file_wd);
                cvs_ent_remove(entlist, cf->file_name);

                cvs_get_repository_path(cf->file_wd, repo, PATH_MAX);

                (void)xsnprintf(attic, PATH_MAX, "%s/%s",
                    repo, CVS_PATH_ATTIC);

                if (mkdir(attic, 0755) == -1 && errno != EEXIST)
                        fatal("cvs_commit_local: failed to create Attic");

                (void)xsnprintf(attic, PATH_MAX, "%s/%s/%s%s", repo,
                    CVS_PATH_ATTIC, cf->file_name, RCS_FILE_EXT);

                if (rename(cf->file_rpath, attic) == -1)
                        fatal("cvs_commit_local: failed to move %s to Attic",
                            cf->file_path);

                if (cvs_server_active == 1)
                        cvs_server_update_entry("Remove-entry", cf);
        }

        if (verbosity > 1)
                cvs_printf("done\n");
        else {
                cvs_log(LP_NOTICE, "checking in '%s'; revision %s -> %s",
                    cf->file_path, rbuf, nbuf);
        }

        if (line_list != NULL) {
                fi = xcalloc(1, sizeof(*fi));
                fi->file_path = xstrdup(cf->file_path);
                fi->crevstr = xstrdup(rbuf);
                fi->nrevstr = xstrdup(nbuf);
                if (tag != NULL)
                        fi->tag_new = xstrdup(tag);
                TAILQ_INSERT_TAIL(&files_info, fi, flist);
        }

        switch (cf->file_status) {
        case FILE_MODIFIED:
                histtype = CVS_HISTORY_COMMIT_MODIFIED;
                break;
        case FILE_ADDED:
                histtype = CVS_HISTORY_COMMIT_ADDED;
                break;
        case FILE_REMOVED:
                histtype = CVS_HISTORY_COMMIT_REMOVED;
                break;
        default:
                histtype = -1;
                break;
        }

        free(crev);

        if (histtype != -1)
                cvs_history_add(histtype, cf, NULL);
        else
                cvs_log(LP_NOTICE, "histtype was -1 for %s", cf->file_path);
}

static BUF *
commit_diff(struct cvs_file *cf, RCSNUM *rev, int reverse)
{
        int fd1, fd2, d;
        char *p1, *p2;
        BUF *b;

        (void)xasprintf(&p1, "%s/diff1.XXXXXXXXXX", cvs_tmpdir);

        if (cf->file_status == FILE_MODIFIED ||
            cf->file_status == FILE_ADDED) {
                b = buf_load_fd(cf->fd);
                fd1 = buf_write_stmp(b, p1, NULL);
                buf_free(b);
        } else {
                fd1 = rcs_rev_write_stmp(cf->file_rcs, rev, p1, 0);
        }

        (void)xasprintf(&p2, "%s/diff2.XXXXXXXXXX", cvs_tmpdir);
        fd2 = rcs_rev_write_stmp(cf->file_rcs, rev, p2, RCS_KWEXP_NONE);

        b = buf_alloc(128);

        diff_format = D_RCSDIFF;

        if (reverse == 1)
                d = diffreg(p2, p1, fd2, fd1, b, D_FORCEASCII);
        else
                d = diffreg(p1, p2, fd1, fd2, b, D_FORCEASCII);
        if (d == D_ERROR)
                fatal("commit_diff: failed to get RCS patch");

        close(fd1);
        close(fd2);

        free(p1);
        free(p2);

        return (b);
}

static void
commit_desc_set(struct cvs_file *cf)
{
        BUF *bp;
        int fd;
        char desc_path[PATH_MAX], *desc;

        (void)xsnprintf(desc_path, PATH_MAX, "%s/%s/%s%s",
            cf->file_wd, CVS_PATH_CVSDIR, cf->file_name, CVS_DESCR_FILE_EXT);

        if ((fd = open(desc_path, O_RDONLY)) == -1)
                return;

        bp = buf_load_fd(fd);
        buf_putc(bp, '\0');
        desc = buf_release(bp);

        rcs_desc_set(cf->file_rcs, desc);

        (void)close(fd);
        (void)cvs_unlink(desc_path);

        free(desc);
}