/*
 * Copyright (c) 1995 Sun Microsystems, Inc.  All Rights Reserved
 *
 * module:
 *	rename.c
 *
 * purpose:
 *	routines to determine whether or not any renames have taken place
 *	and note them (for reconciliation) if we find any
 *
 * contents:
 *	find_renames . look for files that have been renamed
 *	find_oldname . (static) find the file we were renamed from
 *	note_rename .. (static) note the rename for subsequent reconciliation
 *
 * notes:
 *	the reason renames warrant special attention is because the tree
 *	we have constructed is name based, and a directory rename can
 *	appear as zillions of changes.  We attempt to find and deal with
 *	renames prior to doing the difference analysis.
 *
 *	The only case we deal with here is simple renames.  If new links
 *	have been created beneath other directories (i.e. a file has been
 *	moved from one directory to another), the generalized link finding
 *	stuff will deal with it.
 *
 *	This is still under construction, and to completely deal with
 *	directory renames may require some non-trivial tree restructuring.
 *	There is a whole design note on this subject.  In the mean time,
 *	we still detect file renames, so that the user will see them
 *	reported as "mv"s rather than as "ln"s and "rm"s.  Until directory
 *	renames are fully implemented, they will instead be handled as
 *	mkdirs, massive links and unlinks, and rmdirs.
 */
#ident	"@(#)rename.c	1.5	96/06/13 SMI"

#include <stdio.h>

#include "filesync.h"
#include "database.h"


/* local routines */
static struct file *find_oldname(struct file *, struct file *, side_t);
static errmask_t
	note_rename(struct file *, struct file *, struct file *, side_t);

/*
 * routine:
 *	find_renames
 *
 * purpose:
 *	recursively perform rename analysis on a directory
 *
 * parameters:
 *	file node for the suspected directory
 *
 * returns:
 *	error mask
 *
 * note:
 *	the basic algorithm here is to search every directory
 *	for files that have been newly created on one side,
 *	and then look to see if they correspond to an identical
 *	file that has been newly deleted on the same side.
 */
errmask_t
find_renames(struct file *fp)
{	struct file *np, *rp;
	errmask_t errs = 0;
	int stype, dtype, btype, side;

	/* if this isn't a directory, there is nothing to analyze	*/
	if (fp->f_files == 0)
		return (0);

	/* look for any files under this directory that may have been renamed */
	for (np = fp->f_files; np; np = np->f_next) {
		btype = np->f_info[OPT_BASE].f_type;
		stype = np->f_info[OPT_SRC].f_type;
		dtype = np->f_info[OPT_DST].f_type;

		/* a rename must be a file that is new on only one side */
		if (btype == 0 && stype != dtype && (!stype || !dtype)) {
			side = stype ? OPT_SRC : OPT_DST;
			rp = find_oldname(fp, np, side);
			if (rp)
				errs |= note_rename(fp, np, rp, side);
		}
	}

	/* recursively examine all my children			*/
	for (np = fp->f_files; np; np = np->f_next) {
		errs |= find_renames(np);
	}

	return (errs);
}

/*
 * routine:
 *	find_oldname
 *
 * purpose:
 *	to search for an old name for a newly discovered file
 *
 * parameters:
 *	file node for the containing directory
 *	file node for the new file
 *	which side the rename is believed to have happened on
 *
 * returns:
 *	pointer to likely previous file
 *	0	no candidate found
 *
 * note:
 *	this routine only deals with simple renames within a single
 *	directory.
 */
static struct file *find_oldname(struct file *dirp, struct file *new,
	side_t side)
{	struct file *fp;
	long maj, min;
	ino_t inum;
	off_t size;
	side_t otherside = (side == OPT_SRC) ? OPT_DST : OPT_SRC;

	/* figure out what we're looking for		*/
	inum = new->f_info[side].f_ino;
	maj  = new->f_info[side].f_d_maj;
	min  = new->f_info[side].f_d_min;
	size = new->f_info[side].f_size;

	/*
	 * search the same directory for any entry that might describe
	 * the previous name of the new file.
	 */
	for (fp = dirp->f_files; fp; fp = fp->f_next) {
		/* previous name on changed side must no longer exist	*/
		if (fp->f_info[side].f_type != 0)
			continue;

		/* previous name on the other side must still exist	*/
		if (fp->f_info[otherside].f_type == 0)
			continue;

		/* it must describe the same inode as the new file	*/
		if (fp->f_info[OPT_BASE].f_type != new->f_info[side].f_type)
			continue;	/* must be same type		*/
		if (((side == OPT_SRC) ? fp->f_s_inum : fp->f_d_inum) != inum)
			continue;	/* must be same inode #		*/
		if (((side == OPT_SRC) ? fp->f_s_maj : fp->f_d_maj) != maj)
			continue;	/* must be same major #		*/
		if (((side == OPT_SRC) ? fp->f_s_min : fp->f_d_min) != min)
			continue;	/* must be same minor #		*/

		/*
		 * occasionally a prompt delete and create can reuse the
		 * same i-node in the same directory.  What we really
		 * want is generation, but that isn't available just
		 * yet, so our poor-man's approximation is the size.
		 * There is little point in checking ownership and
		 * modes, since the fact that it is in the same
		 * directory strongly suggests that it is the same
		 * user who is doing the deleting and creating.
		 */
		if (fp->f_info[OPT_BASE].f_size != size)
			continue;

		/* looks like we found a match				*/
		return (fp);
	}

	/* no joy	*/
	return (0);
}

/*
 * routine:
 *	note_rename
 *
 * purpose:
 *	to record a discovered rename, so that the reconciliation
 *	phase will deal with it as a rename rather than as link
 *	followed by an unlink.
 *
 * parameters:
 *	file node for the containing directory
 *	file node for the new file
 *	file node for the old file
 *	which side the rename is believed to have happened on
 *
 * returns:
 *	error mask
 */
static errmask_t
note_rename(struct file *dirp, struct file *new,
			struct file *old, side_t side)
{
	int dir;
	errmask_t errs = 0;
	static char *sidenames[] = {"base", "source", "dest"};

	dir = new->f_info[side].f_type == S_IFDIR;

	if (opt_debug & DBG_ANAL)
		fprintf(stderr, "ANAL: NOTE RENAME %s %s/%s -> %s/%s on %s\n",
			dir ? "directory" : "file",
			dirp->f_name, old->f_name, dirp->f_name, new->f_name,
			sidenames[side]);

	/* FIX: we don't deal with directory renames yet	*/
	if (dir)
		return (0);

	/* note that a rename has taken place			*/
	if (side == OPT_SRC) {
		new->f_srcdiffs |= D_RENAME_TO;
		old->f_srcdiffs |= D_RENAME_FROM;
	} else {
		new->f_dstdiffs |= D_RENAME_TO;
		old->f_dstdiffs |= D_RENAME_FROM;
	}

	/* put a link to the old name in the new name		*/
	new->f_previous = old;

	/* for most files, there is nothing else we have to do	*/
	if (!dir)
		return (errs);

	/*
	 * FIX ... someday we are going to have to merge the old and
	 *	   new children into a single tree, but there are
	 *	   horrendous backout problems if we are unable to
	 *	   do the mvdir, so I have postponed this feature.
	 */

	return (errs);
}
