/*
** 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 1, 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, write to the Free Software
** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

/*
 * Author : Alexandre Parenteau <aubonbeurre@hotmail.com> --- December 1997
 */

/*
 * CvsCommands.cpp --- set of CVS commands
 */

#include "stdafx.h"

#include <errno.h>

#ifdef WIN32
#	include <process.h>
#	include "wincvs.h"

#	ifdef _DEBUG
#	define new DEBUG_NEW
#	undef THIS_FILE
	static char THIS_FILE[] = __FILE__;
#	endif
#endif /* WIN32 */

#include "CvsCommands.h"
#include "CvsArgs.h"
#include "MultiFiles.h"
#include "PromptFiles.h"
#include "AppConsole.h"
#include "AppGlue.h"
#include "MoveToTrash.h"
#include "LineCmd.h"
#include "CvsAlert.h"
#include "CommitDlg.h"
#include "ImportDlg.h"
#include "CheckoutDlg.h"
#include "TclGlue.h"
#include "ImportFilterDlg.h"
#include "UpdateDlg.h"
#include "LogDlg.h"
#include "DiffDlg.h"
#include "LogParse.h"
#include "RtagDlg.h"
#include "TagDlg.h"
#include "InitDlg.h"
#include "ReleaseDlg.h"
#include "AnnotateDlg.h"
#include "StatusDlg.h"
#include "LoginDlg.h"
#include "BrowseViewHandlers.h"
#include "LaunchHandlers.h"
#include "FileActionDlg.h"
#include "Authen.h"
	
#ifdef qMacCvsPP
#	include "AppGlue.mac.h"
#	include "MacMisc.h"
#	include "MacCvsApp.h"
#	include "GUSIFileSpec.h"
#	include <LaunchServices.h>
#endif

#ifdef qUnix
#include "UCvsApp.h"
#endif

/*!
	Get the edit type name
	\param editType Edit type
	\return The edit type name
*/
const char* GetEditTypeName(const kEditCmdType editType)
{
	const char* res = _i18n("Unknown");

	switch( editType )
	{
	case kEditNormal:
		res = _i18n("Edit");
		break;
	case kEditReserved:
		res = _i18n("Reserved Edit");
		break;
	case kEditForce:
		res = _i18n("Force Edit");
		break;
	default:
		break;
	}

	return res;
}

/*!
	Get the tag type name
	\param tagType Edit type
	\return The tag type name
*/
const char* GetTagTypeName(const kTagCmdType tagType)
{
	const char* res = _i18n("Unknown");

	switch( tagType )
	{
	case kTagCreate:
		res = _i18n("Create Tag");
		break;
	case kTagDelete:
		res = _i18n("Delete Tag");
		break;
	case kTagBranch:
		res = _i18n("Branch");
		break;
	default:
		break;
	}

	return res;
}

/*!
	Get the tag type name
	\param tagType Edit type
	\return The tag type name
*/
const char* GetRtagTypeName(const kTagCmdType tagType)
{
	const char* res = _i18n("Unknown");

	switch( tagType )
	{
	case kTagCreate:
		res = _i18n("Rtag Create");
		break;
	case kTagDelete:
		res = _i18n("Rtag Delete");
		break;
	case kTagBranch:
		res = _i18n("RTag Branch");
		break;
	default:
		break;
	}

	return res;
}

/*!
	Execute admin macro
	\param macroName Macro name to be executed
*/
void CvsCmdMacrosAdmin(const char* macroName)
{
	if( 
#if INTERNAL_AUTHEN
		gCvsPrefs.empty() || 
#endif /* INTERNAL_AUTHEN */
		!CTcl_Interp::IsAvail() )
		return;
	
	CTcl_Interp interp;
	CStr path = macroName;
	CTcl_Interp::Unixfy(path);
	interp.DoScriptVar("source \"%s\"", (const char*)path);
}

/*!
	Verify the selection handler is properly setup to carry on launching command
	\param handler Command handler to be tested
	\param mf Returned selection as MultiFiles
	\param forceNormalize specifies whether to force normalization of the MultiFiles or rely on the selection handler's requirements
	\return true if handler is properly setup, false otherwise
*/
static bool CheckSelectionHandler(KiSelectionHandler& handler, MultiFiles*& mf, bool forceNormalize=false)
{
#if INTERNAL_AUTHEN
	if( gCvsPrefs.empty() )
	{
		return false;
	}
#endif /* INTERNAL_AUTHEN */

	mf = handler.GetSelection();
	if( !mf && handler.GetNeedSelection() )
	{
		return false;
	}

	return ( forceNormalize || handler.NeedsNormalize() ) ? mf->Normalize() : true;
}

/*!
	Execute <b>cvs init</b> command
*/
void CvsCmdInit()
{
#if INTERNAL_AUTHEN
	if( gCvsPrefs.empty() )
		return;
#endif /* INTERNAL_AUTHEN */
	
	bool forceCvsroot;
	CStr cvsroot;

	if( !CompatGetInit(forceCvsroot, cvsroot) )
		return;

	CvsArgs args;

	if( forceCvsroot )
	{
#if INTERNAL_AUTHEN
		args.addcvsroot();
#else
		args.add("-d");
		args.add(cvsroot);
#endif /* INTERNAL_AUTHEN */
	}

	args.add("init");
	
	args.print();
	launchCVS(0L, args.Argc(), args.Argv());
}

/*!
	Execute <b>cvs login</b> command
*/
void CvsCmdLogin(void)
{
#if INTERNAL_AUTHEN
	if( gCvsPrefs.empty() )
		return;
#endif /* INTERNAL_AUTHEN */
	
	bool forceCvsroot;
	CStr cvsroot;

	if( !CompatGetLogin(forceCvsroot, cvsroot) )
		return;

#if INTERNAL_AUTHEN
	AuthenModel* curModel = AuthenModel::GetInstance(gAuthen.kind());
	if( !curModel->HasLogin() )
	{
		cvs_err(_i18n("Logging in is required for 'pserver' or 'sspi' authentication only (see Preferences dialog).\n"
			"Please consult the CVS manual for more details.\n"));
	}
#endif /* INTERNAL_AUTHEN */
	
	if( gCvsPrefs.HasLoogedIn() && gCvsPrefs.LogoutTimeOut() > 0 )
	{
		cvs_err(_i18n("Making automatic logout:\n"));
		CvsCmdLogout();
		WaitForCvs();
	}
	
	gCvsPrefs.SetHasLoogedIn(true);

	CvsArgs args;

	if( forceCvsroot )
	{
#if INTERNAL_AUTHEN
		args.addcvsroot();
#else
		args.add("-d");
		args.add(cvsroot);
#endif /* INTERNAL_AUTHEN */
	}

	args.add("login");

	args.print();
	launchCVS(0L, args.Argc(), args.Argv());
}

/*!
	Execute <b>cvs logout</b> command
*/
void CvsCmdLogout(void)
{
#if INTERNAL_AUTHEN
	if( gCvsPrefs.empty() )
		return;

	AuthenModel* curModel = AuthenModel::GetInstance(gAuthen.kind());
	if( !curModel->HasLogin() )
	{
		cvs_err(_i18n("Logging out is required for 'pserver' or 'sspi' authentication only (see Preferences dialog).\n"
			"Please consult the CVS manual for more details.\n"));
	}
#endif /* INTERNAL_AUTHEN */
	
	gCvsPrefs.SetHasLoogedIn(false);

	CvsArgs args;

	args.add("logout");

	args.print();
	launchCVS(0L, args.Argc(), args.Argv());
}

/*!
	Execute <b>cvs rtag</b> command
	\param handler Selection for the command
	\param tagType Tag type
*/
void CvsCmdRtag(KiSelectionHandler& handler, const kTagCmdType tagType)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool norecurs;
	bool movDelBranch;
	bool overwriteExisting;
	bool useMostRecent;
	bool lookAttic;
	CStr modname;
	CStr tagname;
	CStr date;
	CStr rev;
	bool forceCvsroot;
	CStr cvsroot;

	if( !CompatGetRtag(mf, tagType, norecurs, movDelBranch, overwriteExisting, tagname, modname, date, rev, useMostRecent, lookAttic, forceCvsroot, cvsroot) )
		return;

	CvsArgs args;

	if( forceCvsroot )
	{
#if INTERNAL_AUTHEN
		args.addcvsroot();
#else
		args.add("-d");
		args.add(cvsroot);
#endif /* INTERNAL_AUTHEN */
	}

	args.add("rtag");
	
	switch( tagType )
	{
	case kTagCreate:
		// Nothing to do
		break;
	case kTagDelete:
		args.add("-d");
		break;
	case kTagBranch:
		args.add("-b");
		break;
	default:
		break;
	}
	
	if( norecurs )
		args.add("-l");
	
	if( movDelBranch )
		args.add("-B");
	
	if( lookAttic )
		args.add("-a");
	
	if( tagType == kTagCreate || tagType == kTagBranch )
	{
		if( useMostRecent )
			args.add("-f");
		
		if( overwriteExisting )
			args.add("-F");
		
		if( !date.empty() )
		{
			args.add("-D");
			args.add(date);
		}
		
		if( !rev.empty() )
		{
			args.add("-r");
			args.add(rev);
		}
	}

	args.add(tagname);
	args.add(modname);
	
	args.print();
	launchCVS(0L, args.Argc(), args.Argv());
}

/*!
	Execute <b>cvs checkout</b> and <b>cvs export</b> command
	\param handler Selection for the command
*/
void CvsCmdCheckoutModule(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi = mf->begin();
	const char* dir = mfi->getdir();

	bool norecurs;
	CStr modname;
	CStr path(dir);
	bool toStdout;
	CStr date;
	CStr rev;
	bool useMostRecent;
	CStr rev1;
	CStr rev2;
	bool branchPointMerge;
	bool threeWayConflicts;
	bool doexport;
	bool forceCvsroot;
	CStr cvsroot;
	bool overrideCheckoutDir;
	CStr checkoutDir;
	CStr keyword;
	bool resetSticky;
	bool dontShortenPaths;
	bool caseSensitiveNames;
	bool lastCheckinTime;

	if( !CompatGetCheckout(mf, modname, path, norecurs,
		toStdout, date, rev, useMostRecent, rev1, rev2, branchPointMerge, threeWayConflicts, 
		doexport, forceCvsroot, cvsroot, overrideCheckoutDir, checkoutDir, keyword, resetSticky, dontShortenPaths, 
		caseSensitiveNames, lastCheckinTime) )
	{
		return;
	}

	CvsArgs args;

	if( forceCvsroot )
	{
#if INTERNAL_AUTHEN
		args.addcvsroot();
#else
		args.add("-d");
		args.add(cvsroot);
#endif /* INTERNAL_AUTHEN */
	}

	args.add(doexport ? "export" : "checkout");

	if( gCvsPrefs.PruneOption() && !doexport )
		args.add("-P");
	
	if( norecurs )
		args.add("-l");
	
	if( toStdout )
		args.add("-p");
	
	if( caseSensitiveNames )
		args.add("-S");
	
	if( lastCheckinTime )
		args.add("-t");
	
	if( resetSticky )
	{
		args.add("-A");
	}
	else
	{
		if( !keyword.empty() )
		{
			args.add(keyword);
		}
		
		if( useMostRecent )
		{
			args.add("-f");
		}
		
		if( !date.empty() )
		{
			args.add("-D");
			args.add(date);
		}
		
		if( !rev.empty() )
		{
			args.add("-r");
			args.add(rev);
		}
	}

	if( !rev1.empty() )
	{
		if( branchPointMerge )
			args.add("-b");
		
		if( threeWayConflicts )
			args.add("-3");
		
		CStr tmp;
		tmp = "-j";
		tmp << rev1;
		args.add(tmp);
	}
	
	if( !rev2.empty() )
	{
		CStr tmp;
		tmp = "-j";
		tmp << rev2;
		args.add(tmp);
	}
	
	if( doexport && rev.empty() && date.empty() )
	{
		args.add("-r");
		args.add("HEAD");
	}

	if( overrideCheckoutDir )
	{
		if( dontShortenPaths )
			args.add("-N");

		args.add("-d");
		args.add(checkoutDir);
	}

	args.add(modname);
	
	args.print(path);
	launchCVS(path, args.Argc(), args.Argv());
}

/*!
	Execute <b>cvs import</b> command
	\param handler Selection for the command
*/
void CvsCmdImportModule(KiSelectionHandler& handler)
{	
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		const char* dir = mfi->getdir();
		ReportConflict* entries = NULL;
		ReportWarning* warnings = NULL;
		
		if( !CompatImportFilter(dir, entries, warnings) )
			return;
		
		CStr modname, vendortag, reltag, path(dir), vendorBranchId;
		CStr logmsg;
		bool useDefIgnore;
		bool useFilesTime;
		bool createSandbox;
		bool dontCreateVendorTag;
		bool overwriteReleaseTags;
		bool forceCvsroot;
		CStr cvsroot;

		if( !CompatGetImport(mf, entries, warnings, modname, logmsg, vendortag, reltag, vendorBranchId, path, useDefIgnore, useFilesTime, createSandbox, dontCreateVendorTag, overwriteReleaseTags, forceCvsroot, cvsroot) )
			return;
		
		CvsArgs args;
		
		if( forceCvsroot )
		{
#if INTERNAL_AUTHEN
			args.addcvsroot();
#else
			args.add("-d");
			args.add(cvsroot);
#endif /* INTERNAL_AUTHEN */
		}
		
		args.add("import");
		
		if( createSandbox )
		{
			args.add("-C");
		}

		if( useFilesTime )
		{
			args.add("-d");
		}
		
		if( !dontCreateVendorTag && overwriteReleaseTags )
		{
			args.add("-f");
		}

		// Fill the wrappers
		ReportConflict* tmpentry = entries;
		
		if( !useDefIgnore )
		{
			args.add("-I"); // no default (core, *.o...)
			args.add("!");
			args.add("-I"); // but ignore the CVS folders
			args.add("CVS");
		}
		
		while( tmpentry != 0L )
		{
			if( tmpentry->HasIgnore() )
			{
				CPStr ign;
				args.add("-I");
				
				if( tmpentry->IsExtension() )
					ign << '*';
				
				ign << tmpentry->GetPattern();
				args.add(ign);
			}
			else if( !tmpentry->HasForceText() &&
				(tmpentry->IsBinary() || tmpentry->HasForceBinary() || tmpentry->HasForceNoKeywords() || 
				tmpentry->IsUnicode() || tmpentry->HasForceUnicode()) )
			{
				CPStr wrap;
				const char* pattern = tmpentry->GetPattern();
				bool hasSpace = strchr(pattern, ' ') != 0L;
				args.add("-W");
				
				if( hasSpace )
					wrap << '"';
				
				if( tmpentry->IsExtension() )
					wrap << '*';
				
				wrap << pattern;
				
				if( hasSpace )
					wrap << '"';
				
				if( tmpentry->HasForceNoKeywords() )
				{
					wrap << " -k 'o'";
				}
				else if( tmpentry->HasForceBinary() )
				{
					wrap << " -k 'b'";
				}
				else if( tmpentry->HasForceUnicode() )
				{
					wrap << " -k 'u'";
				}
				else if( tmpentry->IsBinary() )
				{
					wrap << " -k 'b'";
				}
				else if( tmpentry->IsUnicode() )
				{
					wrap << " -k 'u'";
				}
				
				args.add(wrap);
			}
			
			tmpentry = tmpentry->next;
		}
		
		free_list_types(warnings, entries);
		
		if( !vendorBranchId.empty() )
		{
			args.add("-b");
			args.add(vendorBranchId);
		}

		args.add("-m");
		args.add(CleanupLogMsg(logmsg));
		
		if( dontCreateVendorTag )
		{
			args.add("-n");
			args.add(modname);
		}
		else
		{
			args.add(modname);
			args.add(vendortag);
			args.add(reltag);
		}

		args.print(path);
		launchCVS(path, args.Argc(), args.Argv());
	}
}

/*!
	Execute command line
	\param handler Selection for the command
*/
void CvsCmdCommandLine(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	CStr dir;
	CStr cmd;
	bool addDefault;
	bool addSelection;
	bool forceCvsroot;
	CStr cvsroot;

	if( !CompatGetCommand(mf, cmd, dir, addDefault, addSelection, forceCvsroot, cvsroot) )
		return;

	char* argv[MAX_ARGS];
	int argc = StringToArgv(cmd.c_str(), argv);
	
	if( addSelection )
	{
		MultiFiles::const_iterator mfi;
		for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
		{
			CvsArgs args(addDefault);
			
			if( addDefault && forceCvsroot )
			{
#if INTERNAL_AUTHEN
				args.addcvsroot();
#else
				args.add("-d");
				args.add(cvsroot);
#endif /* INTERNAL_AUTHEN */
			}

			for(int i = 0; i < argc; i++)
			{
				if( addDefault && i == 0 && strcmp(argv[i], CVS_CMD) == 0 )
				{
					// Skip the "cvs" arguments as it's already added by args constructor
					continue;
				}

				args.add(argv[i]);
				
				if( !addDefault && forceCvsroot && i == 0 && strcmp(argv[i], CVS_CMD) == 0 )
				{
#if INTERNAL_AUTHEN
					args.addcvsroot();
#else
					args.add("-d");
					args.add(cvsroot);
#endif /* INTERNAL_AUTHEN */
				}
			}
			
			const char* dir = mfi->add(args);
			args.print(dir);
			launchCVS(dir, args.Argc(), args.Argv());
		}
	}
	else
	{
		CvsArgs args(addDefault);
		
		if( addDefault && forceCvsroot )
		{
#if INTERNAL_AUTHEN
			args.addcvsroot();
#else
			args.add("-d");
			args.add(cvsroot);
#endif /* INTERNAL_AUTHEN */
		}

		for(int i = 0; i < argc; i++)
		{
			if( addDefault && i == 0 && strcmp(argv[i], CVS_CMD) == 0 )
			{
				// Skip the "cvs" arguments as it's already added by args constructor
				continue;
			}
			
			args.add(argv[i]);
			
			if( !addDefault && forceCvsroot && i == 0 && strcmp(argv[i], CVS_CMD) == 0 )
			{
#if INTERNAL_AUTHEN
				args.addcvsroot();
#else
				args.add("-d");
				args.add(cvsroot);
#endif /* INTERNAL_AUTHEN */
			}
		}
		
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs update</b> command
	\param handler Selection for the command
*/
void CvsCmdCancelChanges(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		RemoveCvsArgs args;
		args.add("update");

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs update</b> command
	\param handler Selection for the command
	\param queryOnly true for query update, false for the update command
*/
void CvsCmdUpdate(KiSelectionHandler& handler, bool queryOnly)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool toStdout;
	bool noRecurs;
	bool resetSticky;
	CStr date;
	CStr rev;
	bool useMostRecent;
	CStr rev1;
	CStr rev2;
	bool branchPointMerge;
	bool threeWayConflicts;
	bool createMissDir;
	bool getCleanCopy;
	CStr keyword;
	bool caseSensitiveNames;
	bool lastCheckinTime;

	if( !queryOnly && !CompatGetUpdate(mf, toStdout, noRecurs, resetSticky,
		date, rev, useMostRecent, rev1, rev2, branchPointMerge, threeWayConflicts, 
		createMissDir, getCleanCopy, keyword, caseSensitiveNames, lastCheckinTime) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;

		if( queryOnly )
			args.add("-n");
		
		args.add("update");

		if( !keyword.empty() )
		{
			args.add(keyword);
		}

		if( gCvsPrefs.PruneOption() )
			args.add("-P");

		if( !queryOnly )
		{
			if( caseSensitiveNames )
				args.add("-S");
			
			if( lastCheckinTime )
				args.add("-t");
			
			if( resetSticky )
				args.add("-A");
		
			if( createMissDir )
				args.add("-d");
			
			if( getCleanCopy )
				args.add("-C");
			
			if( !date.empty() )
			{
				args.add("-D");
				args.add(date);
			}
			
			if( useMostRecent )
				args.add("-f");
			
			if( !rev1.empty() )
			{
				if( branchPointMerge )
					args.add("-b");

				if( threeWayConflicts )
					args.add("-3");

				CStr tmp;
				tmp = "-j";
				tmp << rev1;
				args.add(tmp);
			}
			
			if( !rev2.empty() )
			{
				CStr tmp;
				tmp = "-j";
				tmp << rev2;
				args.add(tmp);
			}
			
			if( noRecurs )
				args.add("-l");
			
			if( toStdout )
				args.add("-p");
			
			if( !rev.empty() )
			{
				args.add("-r");
				args.add(rev);
			}
		}

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs commit</b> command
	\param handler Selection for the command
*/
void CvsCmdCommit(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool norecurs;
	CStr logmsg;
	bool forceCommit;
	bool forceRecurse;
	CStr rev;
	bool noModuleProgram;
	bool checkValidEdits;	

	if( !CompatGetCommit(mf, logmsg, norecurs, forceCommit, forceRecurse, rev, noModuleProgram, checkValidEdits) )
		return;
	
	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("commit");

		if( checkValidEdits )
			args.add("-c");
		
		if( norecurs )
			args.add("-l");
		
		if( noModuleProgram )
			args.add("-n");
		
		if( forceCommit )
			args.add("-f");
		
		if( forceRecurse )
			args.add("-R");
		
		args.add("-m");
		args.add(CleanupLogMsg(logmsg));
		
		if( !rev.empty() )
		{
			args.add("-r");
			args.add(rev);
		}

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs diff</b> or <b>external diff</b> command
	\param handler Selection for the command
	\param diffType Diff command type
*/
void CvsCmdDiff(KiSelectionHandler& handler, kDiffCmdType diffType)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool noRecurs;
	bool isDate1;
	bool isDate2;
	bool useExtDiff;
	CStr rev1;
	CStr rev2;
	bool unifiedDiff;
	bool noWhiteSpace;
	bool noBlankLines;
	bool ignoreCase;
	bool extDiffResult;
	CStr diffOptions;
	CStr keyword;
	CStr  diffTool;

	if( diffType == kDiffGraph )
	{
		// determine initial revision settings
		if( mf->TotalNumFiles() > 0 )
		{
			const MultiFilesEntry& mfe = *mf->begin();
			CStr ignorePath, ignoreName, rev;

			switch( mfe.NumFiles() )
			{
			case 2:
				if( mfe.get(1, ignorePath, ignoreName, rev) ) 
					rev2 = rev;
				
				// fall thru
			case 1:
				if( mfe.get(0, ignorePath, ignoreName, rev) ) 
					rev1 = rev;

				break;
			}
		}
	}
	
	if( !CompatGetDiff(mf, diffType, noRecurs, isDate1, isDate2, rev1, rev2, useExtDiff, diffTool,
		unifiedDiff, noWhiteSpace, noBlankLines, ignoreCase, keyword, diffOptions, 
		mf->TotalNumFiles() > 0) )
	{
		return;
	}

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		extDiffResult = false;

		if( mfi->NumFiles() && useExtDiff )
		{
			CStr path, filename, ext, base, currRev;
			CStr file1, file2;

			for(int i = 0; ; i++)
			{
				if( !mfi->get(i, path, filename, currRev) )
					break;

				if( 0 < i && kDiffGraph == diffType )
				{
					// Graph diff contains two files, but it's actually the same one
					break;
				}

				// note that filename can contain subfolders
				
				GetExtension(filename, base, ext);

				CvsArgs args;
				CStr tmpf(base);
				tmpf << '_';
				
				if( !rev1.empty() )
					tmpf << rev1;
				else
					tmpf << currRev;
				
				tmpf << '_';

				// convert subfolder names to regular name
				FlatenSubFolders(tmpf);

				args.add("update");
				args.add("-p");
				
				if( !rev1.empty() )
				{
					args.add(isDate1 ? "-D" : "-r");
					args.add(rev1);
				}
 				else if( !currRev.empty() ) 
				{
 					args.add("-r");
					args.add(currRev);
  				}

				args.add(filename);
				args.print(path);
				file1 = launchCVS(path, args, (const char *)tmpf, ext);

				if( rev2.empty() )
				{
					file2 = path;
					
					if( !file2.endsWith(kPathDelimiter) )
						file2 << kPathDelimiter;
					
					file2 << filename;
				}
				else
				{
					CvsArgs args2;

					tmpf = base;
					tmpf << '_';
					tmpf << rev2;
					tmpf << '_';

					// convert subfolder names to regular name
					FlatenSubFolders(tmpf);

					args2.add("update");
					args2.add("-p");
					args2.add(isDate2 ? "-D" : "-r");
					args2.add(rev2);
					args2.add(filename);
					args2.print(path);
					file2 = launchCVS(path, args2, (const char *)tmpf, ext);
				}

				if( file1.empty() || file2.empty() )
					continue;

				extDiffResult = LaunchDiff(file1, file2, diffTool, unifiedDiff, noWhiteSpace, noBlankLines, ignoreCase);
			}
		}

		if( !mfi->NumFiles() || !useExtDiff || !extDiffResult )
		{
			CvsArgs args;
			args.add("diff");

			if( noRecurs )
				args.add("-l");

			if( !rev1.empty() )
			{
				args.add(isDate1 ? "-D" : "-r");
				args.add(rev1);
			}
			
			if( !rev2.empty() )
			{
				args.add(isDate2 ? "-D" : "-r");
				args.add(rev2);
			}
			
			if( unifiedDiff )
				args.add("-u");	    // --unified, -u

			if( noWhiteSpace )
				args.add("-wb");	//--ignore-all-space --ignore-space-change

			if( noBlankLines )
				args.add("-B");		//--ignore-blank-lines

			if( ignoreCase )
				args.add("-i");		//--ignore-case

			if( !keyword.empty() )
			{
				args.add(keyword);
			}

			if( !diffOptions.empty() )
			{
				args.parse(diffOptions);
			}

			const char* dir = mfi->add(args);
			args.print(dir);
			launchCVS(dir, args.Argc(), args.Argv());
		}
	}
}

/*!
	Execute <b>cvs add</b> command
	\param handler Selection for the command
	\param addType Add type
*/
void CvsCmdAdd(KiSelectionHandler& handler, kAddCmdType addType /*= kAddNormal*/)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		if( mfi->NumFiles() > 0 )
		{
			// Files
			ControlCvsArgs* args = NULL;
			switch( addType )
			{
			default:
			case kAddNormal:
				args = new AddCvsArgs;
				break;
			case kAddBinary:
				args = new AddBinCvsArgs;
				break;
			case kAddUnicode:
				args = new AddUnicodeCvsArgs;
				break;
			}

			if( !args )
			{
				cvs_err("Unable to allocate memory for arguments list\n");
				return;
			}

			args->add("add");
			
			if( addType == kAddBinary )
			{
				args->add("-kb");
			}
			else if( addType == kAddUnicode )
			{
				args->add("-ku");
			}

			const char* dir = mfi->add(*args);
			
			CvsAlert cvsAlert(kCvsAlertWarningIcon, 
				_i18n("Do you want to continue?"), _i18n("Some problems have been found while adding files and should be corrected. See the console window for details."),
				BUTTONTITLE_CONTINUE, BUTTONTITLE_CANCEL);

			if( !args->HasProblem() || cvsAlert.ShowAlert() == kCvsAlertOKButton )
			{
				args->print(dir);
				launchCVS(dir, args->Argc(), args->Argv());
			}

			delete args;
		}
		else
		{
			// Directory
			const char* dir = mfi->getdir();
			CStr uppath, folder;

			if( !SplitPath(dir, uppath, folder) )
			{
				cvs_err(_i18n("Unable to convert path name '%s'\n"), dir);
				return;
			}
			
			CvsArgs args;
			args.add("add");
			args.add(folder);
			
			args.print(uppath);
			launchCVS(uppath, args.Argc(), args.Argv());
		}
	}
}

/*!
	Execute <b>cvs remove</b> command
	\param handler Selection for the command
*/
void CvsCmdRemove(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		RemoveCvsArgs args;
		
		CompatBeginMoveToTrash();
		
		args.add("remove");
		const char* dir = mfi->add(args);
		args.print(dir);
		
		CompatEndMoveToTrash();
		
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs log</b> or <b>graph</b> command
	\param handler Selection for the command
	\param outGraph true for the graph
	\param defWnd Default window
*/
void CvsCmdLog(KiSelectionHandler& handler, bool outGraph, void* defWnd)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool noRecurs;
	bool defBranch;
	CStr date;
	bool headerOnly;
	bool headerDesc;
	bool supress;
	bool noTags;
	bool onlyRCSfile;
	bool hasRev;
	CStr rev;
	CStr states;
	bool hasUser;
	CStr users;

	if( !CompatGetLog(mf, noRecurs, defBranch, date,
		headerOnly, headerDesc, supress, 
		noTags, onlyRCSfile, hasRev,
		rev, states, hasUser, users,
		outGraph) )
	{
		return;
	}

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("log");

		if( noRecurs )
			args.add("-l");

		if( defBranch )
			args.add("-b");

		if( headerDesc )
		{
			args.add("-t");
		}
		else
		{
			if( headerOnly )
				args.add("-h");
		}

		if( supress )
			args.add("-S");

		if( noTags )
			args.add("-N");

		if( onlyRCSfile )
			args.add("-R");

		if( !date.empty() )
		{
			args.add("-d");
			args.add(date);
		}

		if( !states.empty() )
		{
			args.add("-s");
			args.add(states);
		}

		if( hasRev )
		{
			CStr tmp;
			tmp = "-r";
			tmp << rev;
			args.add(tmp);
		}

		if( hasUser )
		{
			CStr tmp;
			tmp = "-w";
			tmp << users;
			args.add(tmp);
		}

		const char* dir = mfi->add(args);
		args.print(dir);
		CvsLogParse(dir, args, outGraph, defWnd);
	}
}

/*!
	Execute <b>cvs status</b> command
	\param handler Selection for the command
*/
void CvsCmdStatus(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool noRecurs = false;
	STATUSOUTPUT_TYPE outputType = STATUSOUTPUT_VERBOSE;

	if( !CompatGetStatus(mf, noRecurs, outputType) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("status");

		if( noRecurs )
			args.add("-l");

		if( STATUSOUTPUT_VERBOSE == outputType )
		{
			args.add("-v");
		}
		else if( STATUSOUTPUT_QUICK == outputType )
		{
			args.add("-q");
		}

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs admin -l</b> command
	\param handler Selection for the command
*/
void CvsCmdLock(KiSelectionHandler& handler)
{	
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("admin");
		args.add("-l");

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs admin -u</b> command
	\param handler Selection for the command
*/
void CvsCmdUnlock(KiSelectionHandler& handler)
{	
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("admin");
		args.add("-u");

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs tag</b> command
	\param handler Selection for the command
	\param tagType Tag type
*/
void CvsCmdTag(KiSelectionHandler& handler, const kTagCmdType tagType)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;
	
	bool noRecurs;
	bool movDelBranch;
	bool overwriteExisting;
	bool checkUnmod;
	CStr tagName;
	
	if( !CompatGetTag(mf, tagType, noRecurs, movDelBranch, overwriteExisting, tagName, checkUnmod) )
		return;
	
	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("tag");
		
		switch( tagType )
		{
		case kTagCreate:
			// Nothing to do
			break;
		case kTagDelete:
			args.add("-d");
			break;
		case kTagBranch:
			args.add("-b");
			break;
		default:
			break;
		}

		if( noRecurs )
			args.add("-l");
		
		if( movDelBranch )
			args.add("-B");
		
		if( tagType == kTagCreate || tagType == kTagBranch )
		{
			if( overwriteExisting )
				args.add("-F");
			
			if( checkUnmod )
				args.add("-c");
		}
		
		args.add(tagName);
		
		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs edit</b> command
	\param handler Selection for the command
	\param editType Edit type
*/
void CvsCmdEdit(KiSelectionHandler& handler, kEditCmdType editType /*= kNormal*/)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("edit");

		switch( editType )
		{
		default:
		case kEditNormal:
			// intentionally blank
			break;
		case kEditReserved:
			args.add("-c");
			break;
		case kEditForce:
			args.add("-f");
			break;
		}
		
		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs unedit</b> command
	\param handler Selection for the command
*/
void CvsCmdUnedit(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("unedit");

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs watch add</b> command
	\param handler Selection for the command
*/
void CvsCmdWatchOn(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("watch");
		args.add("add");

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs watch remove</b> command
	\param handler Selection for the command
*/
void CvsCmdWatchOff(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("watch");
		args.add("remove");

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs release</b> command
	\param handler Selection for the command
*/
void CvsCmdRelease(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	CStr dir;
	if( !mf->getdir(0, dir) )
	{
		return;
	}

	CStr uppath, folder;
	if( !SplitPath(dir, uppath, folder) )
	{
		cvs_err(_i18n("Unable to convert path name '%s'\n"), dir.c_str());
		
		// Even thought something didn't work we will try to continue and put all the responsibility onto the user
		uppath = dir;
		folder = ".";
	}

	kReleaseDelType removeDirectoryOption = kReleaseNoDel;

	if( !CompatGetRelease(mf, dir, removeDirectoryOption) )
	{
		return;
	}

	CvsArgs args;
	args.add("release");
	
	switch( removeDirectoryOption )
	{
	default:
	case kReleaseNoDel:
		break;
	case kReleaseDelDir:
		args.add("-d");
		break;
	case kReleaseDelAll:
		args.add("-d");
		args.add("-f");
		break;
	case kReleaseDelCvsInfo:
		args.add("-e");
		break;
	}

	args.add(folder);

	args.print(uppath);
	launchCVS(uppath, args.Argc(), args.Argv());
}

/*!
	Execute <b>cvs watchers</b> command
	\param handler Selection for the command
*/
void CvsCmdWatchers(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("watchers");

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs editors</b> command
	\param handler Selection for the command
*/
void CvsCmdEditors(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("editors");

		const char* dir = mfi->add(args);
		args.print(dir);
		launchCVS(dir, args.Argc(), args.Argv());
	}
}

/*!
	Execute <b>cvs annotate</b> command
	\param handler Selection for the command
	\param annotateType Annotate command type
*/
void CvsCmdAnnotate(KiSelectionHandler& handler, kAnnotateCmdType annotateType)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool noRecurs;
	bool isDate;
	CPStr rev;
	bool force;

	if( annotateType == kAnnotateGraph )
	{
		if( mf->TotalNumFiles() == 1 )
		{
			const MultiFilesEntry& mfe = *mf->begin();

			if( mfe.NumFiles() == 1 )
			{
			  CStr ignorePath, ignoreName, revision;
			  
			  if( mfe.get(0, ignorePath, ignoreName, revision) ) 
				  rev = revision;
			}
		}
	}
	
	if( !CompatGetAnnotate(mf, annotateType, noRecurs, isDate, rev, force) )
		return;

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CStr path, filename, ext, base, currRev;
		CStr file;

		for(int i = 0; ; i++)
		{
			if( !mfi->get(i, path, filename, currRev) )
				break;
			
			// note that filename can contain subfolders
			
			GetExtension(filename, base, ext);

			CvsArgs args;
			CStr tmpf(base);

			if( !rev.empty() )
			{
				tmpf << "_ann_";
				tmpf << rev;
				tmpf << '_';
			}
			else
				tmpf << "_ann_";

			// convert subfolder names to regular name
			FlatenSubFolders(tmpf);

			args.add("annotate");
			
			if( noRecurs )
			{
				args.add("-l");
			}

			if( force )
			{
				args.add("-f");
			}
			
			if( !rev.empty() )
			{
				args.add(isDate ? "-D" : "-r");
				args.add(rev);
			}

			args.add(filename);
			args.print(path);
			file = launchCVS(path, args, (const char *)tmpf, ext);

			if( file.empty() )
				continue;

			LaunchDefaultEditor(file);
		}
	}
}

/*!
	Execute <b>erase</b> command
	\param handler Selection for the command
*/
void CvsCmdMoveToTrash(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;
	
	const char* primaryMsg = NULL;
	const char* secondaryMsg = NULL;
	kCvsAlertButtonType defaultButton = kCvsAlertCancelButton;
	
#if defined (TARGET_OS_MAC)
	primaryMsg = _i18n("Are you sure you want to move the selected files to the trash?");
	secondaryMsg = _i18n("The files will be moved into the trash just as if you deleted them from the Finder.");
	defaultButton = kCvsAlertOKButton;
#elif defined (WIN32)
	primaryMsg = _i18n("Are you sure you want to move the selected files to the recycle bin?");
#else
	primaryMsg = _i18n("Are you sure you want to remove the selected files?");
#endif

	CvsAlert cvsAlert(kCvsAlertQuestionIcon, 
		primaryMsg, secondaryMsg);

	if( cvsAlert.ShowAlert(defaultButton) != kCvsAlertOKButton )
	{
		return;
	}
	
	CompatBeginMoveToTrash();
	
	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		int count = mfi->NumFiles();
		for(int i = 0; i < count; i++)
		{
			CStr path, fileName, currRev;
			if( mfi->get(i, path, fileName, currRev) )
			{
				UStr fn(path);
				if( !fn.endsWith(kPathDelimiter) )
					fn << kPathDelimiter;
				
				fn << fileName;
				
				if( !CompatMoveToTrash(fn) )
				{
					UStr errText;
					
					// HINT for translate: "Unable to remove '<FILE>' (error <ERROR>)\n"
					errText << _i18n("Unable to remove '") << fn << _i18n("' (error ") << errno << ")\n";
					
					cvs_err( errText.c_str() );
				}
				else
				{
					UStr errText;
#if defined (TARGET_OS_MAC)
					errText << "'" << fn << _i18n("' has been moved successfully to the trash ...\n");
#elif defined (WIN32)
					errText << "'" << fn << _i18n("' has been moved successfully to the recycle bin ...\n");
#else
					errText << "'" << fn << _i18n("' has been removed successfully ...\n");
#endif
					cvs_out( errText.c_str() );
				}
			}
		}
	}
	
	CompatEndMoveToTrash();
}

/*!
	Execute <b>file action</b> command
	\param handler Selection for the command
*/
void CvsCmdFileAction(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;
	
	bool noRecurs = false;
	kLaunchType launchType = kLaunchEdit;
	
	if( !CompatGetFileAction(mf, launchType) )
		return;
	
	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		for( int nIndex = 0; nIndex < mfi->NumFiles(); nIndex++ )
		{
			UStr path;
			UStr fileName;
			UStr currRev;
			
			mfi->get(nIndex, path, fileName, currRev);
			if( !path.empty() && !path.endsWith(kPathDelimiter) )
				path << kPathDelimiter;
			
			path << fileName;
			
			LaunchHandler(launchType, path);
		}
	}
}

/*!
	Execute <b>launch</b> command
	\param handler Selection for the command
	\param launchType Launch type
*/
void CvsCmdLaunch(KiSelectionHandler& handler, const kLaunchType launchType)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;
	
	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		for( int nIndex = 0; nIndex < mfi->NumFiles(); nIndex++ )
		{
			UStr path;
			UStr fileName;
			UStr currRev;
			
			mfi->get(nIndex, path, fileName, currRev);
			if( !path.empty() && !path.endsWith(kPathDelimiter) )
				path << kPathDelimiter;
			
			path << fileName;
			
			LaunchHandler(launchType, path);
		}
	}
}
