/*
 * Copyright (c) 1992, Brian Berliner and Jeff Polk
 * Copyright (c) 1989-1992, Brian Berliner
 * 
 * You may distribute under the terms of the GNU General Public License as
 * specified in the README file that comes with the CVS source distribution.
 * 
 * Patch
 * 
 * Create a Larry Wall format "patch" file between a previous release and the
 * current head of a module, or between two releases.  Can specify the
 * release as either a date or a revision number.
 */

#include "cvs.h"
#include "getline.h"

static RETSIGTYPE patch_cleanup(int sig);
static Dtype patch_dirproc (void *callerdat, char *dir,
				   char *repos, char *update_dir,
				   List *entries, const char *virtual_repository, Dtype hint);
static int patch_fileproc (void *callerdat, struct file_info *finfo);
static int patch_proc (int argc, char **argv, const char *xwhere,
		       const char *mwhere, const char *mfile, int shorten,
		       int local_specified, const char *mname, const char *msg);

static int force_tag_match = 1;
static int patch_short = 0;
static int toptwo_diffs = 0;
static int local = 0;
static char *options = NULL;
static char *rev1 = NULL;
static int rev1_validated = 0;
static char *rev2 = NULL;
static int rev2_validated = 0;
static char *date1 = NULL;
static char *date2 = NULL;
static char *tmpfile1 = NULL;
static char *tmpfile2 = NULL;
static char *tmpfile3 = NULL;
static int unidiff = 0;

static const char *const patch_usage[] =
{
    "Usage: %s %s [-flR] [-c|-u] [-s|-t] [-V %%d]\n",
    "    -r rev|-D date [-r rev2 | -D date2] modules...\n",
    "\t-f\tForce a head revision match if tag/date not found.\n",
    "\t-l\tLocal directory only, not recursive\n",
    "\t-R\tProcess directories recursively.\n",
    "\t-c\tContext diffs (default)\n",
    "\t-u\tUnidiff format.\n",
    "\t-s\tShort patch - one liner per file.\n",
    "\t-t\tTop two diffs - last change made to the file.\n",
    "\t-D date\tDate.\n",
    "\t-r rev\tRevision - symbolic or numeric.\n",
    "(Specify the --help global option for a list of other help options)\n",
    NULL
};

int patch (int argc, char **argv)
{
    register int i;
    int c;
    int err = 0;
    DBM *db;

    if (argc == -1)
	usage (patch_usage);

    optind = 0;
    while ((c = getopt (argc, argv, "+:k:cuftsQqlRD:r:")) != -1)
    {
	switch (c)
	{
	    case 'Q':
	    case 'q':
		/* The CVS 1.5 client sends these options (in addition to
		   Global_option requests), so we must ignore them.  */
		if (!server_active)
		    error (1, 0,
			   "-q or -Q must be specified before \"%s\"",
			   command_name);
		break;
	    case 'f':
		force_tag_match = 0;
		break;
	    case 'l':
		local = 1;
		break;
	    case 'R':
		local = 0;
		break;
	    case 't':
		toptwo_diffs = 1;
		break;
	    case 's':
		patch_short = 1;
		break;
	    case 'D':
		if (rev2 != NULL || date2 != NULL)
		    error (1, 0,
		       "no more than two revisions/dates can be specified");
		if (rev1 != NULL || date1 != NULL)
		    date2 = Make_Date (optarg);
		else
		    date1 = Make_Date (optarg);
		break;
	    case 'r':
		if (rev2 != NULL || date2 != NULL)
		    error (1, 0,
		       "no more than two revisions/dates can be specified");
		if (rev1 != NULL || date1 != NULL)
		    rev2 = optarg;
		else
		    rev1 = optarg;
		break;
	    case 'k':
		if (options)
		    xfree (options);
		options = RCS_check_kflag (optarg,true,true);
		break;
	    case 'u':
		unidiff = 1;		/* Unidiff */
		break;
	    case 'c':			/* Context diff */
		unidiff = 0;
		break;
	    case '?':
	    default:
		usage (patch_usage);
		break;
	}
    }
    argc -= optind;
    argv += optind;

    /* Sanity checks */
    if (argc < 1)
	usage (patch_usage);

    if (toptwo_diffs && patch_short)
	error (1, 0, "-t and -s options are mutually exclusive");
    if (toptwo_diffs && (date1 != NULL || date2 != NULL ||
			 rev1 != NULL || rev2 != NULL))
	error (1, 0, "must not specify revisions/dates with -t option!");

    if (!toptwo_diffs && (date1 == NULL && date2 == NULL &&
			  rev1 == NULL && rev2 == NULL))
	error (1, 0, "must specify at least one revision/date!");
    if (date1 != NULL && date2 != NULL)
	if (RCS_datecmp (date1, date2) >= 0)
	    error (1, 0, "second date must come after first date!");

    /* if options is NULL, make it a NULL string */
    if (options == NULL)
	options = xstrdup ("");

    if (current_parsed_root->isremote)
    {
	if (local)
	    send_arg("-l");
	if (!force_tag_match)
	    send_arg("-f");
	if (toptwo_diffs)
	    send_arg("-t");
	if (patch_short)
	    send_arg("-s");
	if (unidiff)
	    send_arg("-u");

	if (rev1)
	    option_with_arg ("-r", rev1);
	if (date1)
	    client_senddate (date1);
	if (rev2)
	    option_with_arg ("-r", rev2);
	if (date2)
	    client_senddate (date2);
	if (options[0] != '\0')
	    option_with_arg ("-k", options);

	{
	    int i;
	    for (i = 0; i < argc; ++i)
		send_arg (argv[i]);
	}

	send_to_server ("rdiff\n", 0);
        return get_responses_and_close ();
    }

    /* clean up if we get a signal */
#ifdef SIGABRT
    SIG_register (SIGABRT, patch_cleanup);
#endif
#ifdef SIGHUP
    SIG_register (SIGHUP, patch_cleanup);
#endif
#ifdef SIGINT
    SIG_register (SIGINT, patch_cleanup);
#endif
#ifdef SIGQUIT
    SIG_register (SIGQUIT, patch_cleanup);
#endif
#ifdef SIGPIPE
    SIG_register (SIGPIPE, patch_cleanup);
#endif
#ifdef SIGTERM
    SIG_register (SIGTERM, patch_cleanup);
#endif

    db = open_module ();
    for (i = 0; i < argc; i++)
	err += do_module (db, argv[i], PATCH, "Patching", patch_proc,
			  (char *) NULL, 0, 0, 0, 0, (char *) NULL);
    close_module (db);
    xfree (options);
    patch_cleanup (0);
    return (err);
}

/*
 * callback proc for doing the real work of patching
 */
/* ARGSUSED */
static int patch_proc (int argc, char **argv, const char *xwhere,
		       const char *mwhere, const char *mfile, int shorten,
		       int local_specified, const char *mname, const char *msg)
{
    char *myargv[2];
    int err = 0;
    int which;
    char *repository;
    char *where;
	const char *message;

    repository = (char*)xmalloc (strlen (current_parsed_root->directory) + strlen (argv[0])
			  + (mfile == NULL ? 0 : strlen (mfile) + 1) + 2);
    (void) sprintf (repository, "%s/%s", current_parsed_root->directory, argv[0]);
    where = (char*)xmalloc (strlen (argv[0]) + (mfile == NULL ? 0 : strlen (mfile) + 1)
		     + 1);
    (void) strcpy (where, argv[0]);

    /* if mfile isn't null, we need to set up to do only part of the module */
    if (mfile != NULL)
    {
	char *cp;
	char *path;

	/* if the portion of the module is a path, put the dir part on repos */
	if ((cp = strrchr (mfile, '/')) != NULL)
	{
	    *cp = '\0';
	    (void) strcat (repository, "/");
	    (void) strcat (repository, mfile);
	    (void) strcat (where, "/");
	    (void) strcat (where, mfile);
	    mfile = cp + 1;
	}

	/* take care of the rest */
	path = (char*)xmalloc (strlen (repository) + strlen (mfile) + 2);
	(void) sprintf (path, "%s/%s", repository, mfile);
	if (isdir (path))
	{
	    /* directory means repository gets the dir tacked on */
	    (void) strcpy (repository, path);
	    (void) strcat (where, "/");
	    (void) strcat (where, mfile);
	}
	else
	{
	    myargv[0] = argv[0];
	    myargv[1] = (char*)mfile;
	    argc = 2;
	    argv = myargv;
	}
	xfree (path);
    }

    /* cd to the starting repository */
    if ( CVS_CHDIR (repository) < 0)
    {
	error (0, errno, "cannot chdir to %s", repository);
	xfree (repository);
	return (1);
    }

	which = W_REPOS;

    if (rev1 != NULL && !rev1_validated)
    {
	tag_check_valid (rev1, argc - 1, argv + 1, local, 0, NULL);
	rev1_validated = 1;
    }
    if (rev2 != NULL && !rev2_validated)
    {
	tag_check_valid (rev2, argc - 1, argv + 1, local, 0, NULL);
	rev2_validated = 1;
    }

    if (! verify_read(repository,NULL,NULL,&message,NULL))
    {
       error (0, 0, "User %s cannot read %s", CVS_Username, fn_root(repository));
		if(message)
			error (0, 0, "%s", message);
       return (1);
    }

    /* start the recursion processor */
    err = start_recursion (patch_fileproc, (FILESDONEPROC) NULL, (PREDIRENTPROC) NULL, patch_dirproc,
			   (DIRLEAVEPROC) NULL, NULL,
			   argc - 1, argv + 1, local,
			   which, 0, 1, where, repository, 1, verify_read);
    xfree (where);
    xfree (repository);

    return (err);
}

/*
 * Called to examine a particular RCS file, as appropriate with the options
 * that were set above.
 */
/* ARGSUSED */
static int patch_fileproc (void *callerdat, struct file_info *finfo)
{
    struct utimbuf t;
    char *vers_tag, *vers_head;
    char *rcs = NULL;
    RCSNode *rcsfile;
    FILE *fp1, *fp2, *fp3;
    int ret = 0;
    int retcode = 0;
    char *file1;
    char *file2;
    char *strippath;
    char *line1, *line2;
    size_t line1_chars_allocated;
    size_t line2_chars_allocated;
    char *cp1, *cp2;
    FILE *fp;
    int line_length;
    char *nrev2;

    line1 = NULL;
    line1_chars_allocated = 0;
    line2 = NULL;
    line2_chars_allocated = 0;

    /* find the parsed rcs file */
    if ((rcsfile = finfo->rcs) == NULL)
    {
	ret = 1;
	goto out2;
    }

    rcs = (char*)xmalloc (strlen (finfo->file) + sizeof (RCSEXT) + 5);
    (void) sprintf (rcs, "%s%s", finfo->file, RCSEXT);
 
    /* Keep 'on branch' so we don't diff against an arbitrary HEAD */
    if(!rev2 && !date2)
    {
	nrev2 = RCS_branch_head(rcsfile, rev1);
	if(!nrev2)
		nrev2 = xstrdup(rev1);
    }
    else
	nrev2=xstrdup(rev2);

    /* if vers_head is NULL, may have been removed from the release */
	vers_head = RCS_getversion (rcsfile, nrev2, date2, force_tag_match,
				    (int *) NULL);
	if (vers_head != NULL && RCS_isdead (rcsfile, vers_head))
	{
	    xfree (vers_head);
	    vers_head = NULL;
	}

    if (toptwo_diffs)
    {
	if (vers_head == NULL)
	{
	    ret = 1;
	    goto out2;
	}

	if (!date1)
	    date1 = (char*)xmalloc (MAXDATELEN);
	*date1 = '\0';
	if (RCS_getrevtime (rcsfile, vers_head, date1, 1) == -1)
	{
	    if (!really_quiet)
		error (0, 0, "cannot find date in rcs file %s revision %s",
		       rcs, vers_head);
	    ret = 1;
	    goto out2;
	}
    }
    vers_tag = RCS_getversion (rcsfile, rev1, date1, force_tag_match,
			       (int *) NULL);
    if (vers_tag != NULL && RCS_isdead (rcsfile, vers_tag))
    {
        xfree (vers_tag);
	vers_tag = NULL;
    }

    if (vers_tag == NULL && vers_head == NULL)
    {
	/* Nothing known about specified revs.  */
	ret = 0;
	goto out2;
    }

    if (vers_tag && vers_head && strcmp (vers_head, vers_tag) == 0)
    {
	/* Not changed between releases.  */
	ret = 0;
	goto out2;
    }

    if (patch_short)
    {
	cvs_output ("File ", 0);
	cvs_output (fn_root(finfo->fullname), 0);
	if (vers_tag == NULL)
	{
	    cvs_output (" is new; current revision ", 0);
	    cvs_output (vers_head, 0);
	    cvs_output ("\n", 1);
	}
	else if (vers_head == NULL)
	{
	    cvs_output (" is removed; not included in ", 0);
	    if (rev2 != NULL)
	    {
		cvs_output ("release tag ", 0);
		cvs_output (rev2, 0);
	    }
	    else if (date2 != NULL)
	    {
		cvs_output ("release date ", 0);
		cvs_output (date2, 0);
	    }
	    else
		cvs_output ("current release", 0);
	    cvs_output ("\n", 1);
	}
	else
	{
	    cvs_output (" changed from revision ", 0);
	    cvs_output (vers_tag, 0);
	    cvs_output (" to ", 0);
	    cvs_output (vers_head, 0);
	    cvs_output ("\n", 1);
	}
	ret = 0;
	goto out2;
    }

    /* Create 3 empty files.  I'm not really sure there is any advantage
     * to doing so now rather than just waiting until later.
     *
     * There is - cvs_temp_file opens the file so that it can guarantee that
     * we have exclusive write access to the file.  Unfortunately we spoil that
     * by closing it and reopening it again.  Of course any better solution
     * requires that the RCS functions accept open file pointers rather than
     * simple file names.
     */
    if ((fp1 = cvs_temp_file (&tmpfile1)) == NULL)
    {
	error (0, errno, "cannot create temporary file %s", tmpfile1);
	ret = 1;
	goto out;
    }
    else
	if (fclose (fp1) < 0)
	    error (0, errno, "warning: cannot close %s", tmpfile1);
    if ((fp2 = cvs_temp_file (&tmpfile2)) == NULL)
    {
	error (0, errno, "cannot create temporary file %s", tmpfile2);
	ret = 1;
	goto out;
    }
    else
	if (fclose (fp2) < 0)
	    error (0, errno, "warning: cannot close %s", tmpfile2);
    if ((fp3 = cvs_temp_file (&tmpfile3)) == NULL)
    {
	error (0, errno, "cannot create temporary file %s", tmpfile3);
	ret = 1;
	goto out;
    }
    else
	if (fclose (fp3) < 0)
	    error (0, errno, "warning: cannot close %s", tmpfile3);

    if (vers_tag != NULL)
    {
	retcode = RCS_checkout (rcsfile, (char *) NULL, vers_tag,
				rev1, options, tmpfile1,
				(RCSCHECKOUTPROC) NULL, (void *) NULL, NULL);
	if (retcode != 0)
	{
	    error (0, 0,
		   "cannot check out revision %s of %s", vers_tag, rcs);
	    ret = 1;
	    goto out;
	}
	memset ((char *) &t, 0, sizeof (t));
	if ((t.actime = t.modtime = RCS_getrevtime (rcsfile, vers_tag,
						    (char *) 0, 0)) != -1)
	    /* I believe this timestamp only affects the dates in our diffs,
	       and therefore should be on the server, not the client.  */
	    (void) utime (tmpfile1, &t);
    }
    else if (toptwo_diffs)
    {
	ret = 1;
	goto out;
    }
    if (vers_head != NULL)
    {
	retcode = RCS_checkout (rcsfile, (char *) NULL, vers_head,
				nrev2, options, tmpfile2,
				(RCSCHECKOUTPROC) NULL, (void *) NULL, NULL);
	if (retcode != 0)
	{
	    error (0, 0,
		   "cannot check out revision %s of %s", vers_head, rcs);
	    ret = 1;
	    goto out;
	}
	if ((t.actime = t.modtime = RCS_getrevtime (rcsfile, vers_head,
						    (char *) 0, 0)) != -1)
	    /* I believe this timestamp only affects the dates in our diffs,
	       and therefore should be on the server, not the client.  */
	    (void) utime (tmpfile2, &t);
    }

    switch (diff_exec (tmpfile1, tmpfile2, NULL, NULL, unidiff ? "-u" : "-c", tmpfile3))
    {
	case -1:			/* fork/wait failure */
	    error (1, errno, "fork for diff failed on %s", rcs);
	    break;
	case 0:				/* nothing to do */
	    break;
	case 1:
	    /*
	     * The two revisions are really different, so read the first two
	     * lines of the diff output file, and munge them to include more
	     * reasonable file names that "patch" will understand.
	     */

	    /* Output an "Index:" line for patch to use */
	    cvs_output ("Index: ", 0);
	    cvs_output (fn_root(finfo->fullname), 0);
	    cvs_output ("\n", 1);

	    fp = open_file (tmpfile3, "r");
	    if (getline (&line1, &line1_chars_allocated, fp) < 0 ||
		getline (&line2, &line2_chars_allocated, fp) < 0)
	    {
		if (feof (fp))
		    error (0, 0, "\
failed to read diff file header %s for %s: end of file", tmpfile3, rcs);
		else
		    error (0, errno,
			   "failed to read diff file header %s for %s",
			   tmpfile3, rcs);
		ret = 1;
		if (fclose (fp) < 0)
		    error (0, errno, "error closing %s", tmpfile3);
		goto out;
	    }
	    if (!unidiff)
	    {
		if (strncmp (line1, "*** ", 4) != 0 ||
		    strncmp (line2, "--- ", 4) != 0 ||
		    (cp1 = strchr (line1, '\t')) == NULL ||
		    (cp2 = strchr (line2, '\t')) == NULL)
		{
		    error (0, 0, "invalid diff header for %s", rcs);
		    ret = 1;
		    if (fclose (fp) < 0)
			error (0, errno, "error closing %s", tmpfile3);
		    goto out;
		}
	    }
	    else
	    {
		if (strncmp (line1, "--- ", 4) != 0 ||
		    strncmp (line2, "+++ ", 4) != 0 ||
		    (cp1 = strchr (line1, '\t')) == NULL ||
		    (cp2 = strchr  (line2, '\t')) == NULL)
		{
		    error (0, 0, "invalid unidiff header for %s", rcs);
		    ret = 1;
		    if (fclose (fp) < 0)
			error (0, errno, "error closing %s", tmpfile3);
		    goto out;
		}
	    }
	    assert (current_parsed_root != NULL);
	    assert (current_parsed_root->directory != NULL);
	    {
		strippath = (char*)xmalloc (strlen (current_parsed_root->directory) + 2);
		(void) sprintf (strippath, "%s/", current_parsed_root->directory);
	    }
	    /*else
		strippath = xstrdup (REPOS_STRIP); */
	    if (strncmp (rcs, strippath, strlen (strippath)) == 0)
		rcs += strlen (strippath);
	    xfree (strippath);
	    if (vers_tag != NULL)
	    {
		file1 = (char*)xmalloc (strlen (fn_root(finfo->fullname))
				 + strlen (vers_tag)
				 + 10);
		(void) sprintf (file1, "%s:%s", fn_root(finfo->fullname), vers_tag);
	    }
	    else
	    {
		file1 = xstrdup (DEVNULL);
	    }
	    file2 = (char*)xmalloc (strlen (fn_root(finfo->fullname))
			     + (vers_head != NULL ? strlen (vers_head) : 10)
			     + 10);
	    (void) sprintf (file2, "%s:%s", fn_root(finfo->fullname),
			    vers_head ? vers_head : "removed");

	    /* Note that the string "diff" is specified by POSIX (for -c)
	       and is part of the diff output format, not the name of a
	       program.  */
	    if (unidiff)
	    {
		cvs_output ("diff -u ", 0);
		cvs_output (file1, 0);
		cvs_output (" ", 1);
		cvs_output (file2, 0);
		cvs_output ("\n", 1);

		cvs_output ("--- ", 0);
		cvs_output (file1, 0);
		cvs_output (cp1, 0);
		cvs_output ("+++ ", 0);
	    }
	    else
	    {
		cvs_output ("diff -c ", 0);
		cvs_output (file1, 0);
		cvs_output (" ", 1);
		cvs_output (file2, 0);
		cvs_output ("\n", 1);

		cvs_output ("*** ", 0);
		cvs_output (file1, 0);
		cvs_output (cp1, 0);
		cvs_output ("--- ", 0);
	    }

	    cvs_output (fn_root(finfo->fullname), 0);
	    cvs_output (cp2, 0);

	    /* spew the rest of the diff out */
	    while ((line_length
		    = getline (&line1, &line1_chars_allocated, fp))
		   >= 0)
		cvs_output (line1, 0);
	    if (line_length < 0 && !feof (fp))
		error (0, errno, "cannot read %s", tmpfile3);

	    if (fclose (fp) < 0)
		error (0, errno, "cannot close %s", tmpfile3);
	    xfree (file1);
	    xfree (file2);
	    break;
	default:
	    error (0, 0, "diff failed for %s", fn_root(finfo->fullname));
    }
  out:
    if (line1)
        xfree (line1);
    if (line2)
        xfree (line2);
    if (CVS_UNLINK (tmpfile1) < 0)
	error (0, errno, "cannot unlink %s", tmpfile1);
    if (CVS_UNLINK (tmpfile2) < 0)
	error (0, errno, "cannot unlink %s", tmpfile2);
    if (CVS_UNLINK (tmpfile3) < 0)
	error (0, errno, "cannot unlink %s", tmpfile3);
    xfree (tmpfile1);
    xfree (tmpfile2);
    xfree (tmpfile3);
    tmpfile1 = tmpfile2 = tmpfile3 = NULL;

 out2:
    xfree(nrev2);
    xfree (vers_tag);
    xfree (vers_head);
    xfree (rcs);
    return (ret);
}

/*
 * Print a warm fuzzy message
 */
/* ARGSUSED */
static Dtype patch_dirproc (void *callerdat, char *dir, char *repos, char *update_dir, List *entries, const char *virtual_repository, Dtype hint)
{
	if(hint!=R_PROCESS)
		return hint;

    if (!quiet)
	error (0, 0, "Diffing %s", update_dir);
    return (R_PROCESS);
}

/*
 * Clean up temporary files
 */
static RETSIGTYPE patch_cleanup (int sig)
{
    /* Note that the checks for existence_error are because we are
       called from a signal handler, without SIG_begincrsect, so
       we don't know whether the files got created.  */

    if (tmpfile1 != NULL)
    {
	if (unlink_file (tmpfile1) < 0
	    && !existence_error (errno))
	    error (0, errno, "cannot remove %s", tmpfile1);
	xfree (tmpfile1);
    }
    if (tmpfile2 != NULL)
    {
	if (unlink_file (tmpfile2) < 0
	    && !existence_error (errno))
	    error (0, errno, "cannot remove %s", tmpfile2);
	xfree (tmpfile2);
    }
    if (tmpfile3 != NULL)
    {
	if (unlink_file (tmpfile3) < 0
	    && !existence_error (errno))
	    error (0, errno, "cannot remove %s", tmpfile3);
	xfree (tmpfile3);
    }
    tmpfile1 = tmpfile2 = tmpfile3 = NULL;
}


syntax highlighted by Code2HTML, v. 0.9.1