/*
** 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 <io.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 "Authen.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"
	
#ifdef qMacCvsPP
#	include "AppGlue.mac.h"
#	include "MacMisc.h"
#endif

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

/*!
	Clean up the log message for the server and put the good line feeds
	\param msg Message to be cleaned
	\return The cleaned message
	\todo mac : ISO conversion
*/
char* CleanupLogMsg(const char* msg)
{
	static CStaticAllocT<char> buf;

	size_t len = strlen(msg);
	buf.AdjustSize(len + 1);

	char* tmp = buf;
	char c;


	while( (c = *msg++) != '\0' )
	{
#ifdef WIN32
		if(c == '\r')
			continue;
#endif /* WIN32 */
#ifdef TARGET_OS_MAC
		if(c == '\r')
			c = '\n';
#endif /* TARGET_OS_MAC */
		*tmp++ = c;
	}

	*tmp++ = '\0';

	return buf;
}

/*!
	Split the path into the up-directory and the folder
	\param dir Path to be split
	\param uppath Return up-directory
	\param folder Return folder or file
	\param dontSkipDelimiter Set to true to prevent skipping the delimiter if the path to be split ends with one
	\return true on success, false otherwise
	\note Passing NULL is considered an error, passing empty string returns true
*/
bool SplitPath(const char* dir, CStr& uppath, CStr& folder, bool dontSkipDelimiter /*= false*/)
{
	char newDir[MAX_PATH * 2] = "";
	char oldDir[MAX_PATH * 2] = "";
	char newFile[MAX_PATH] = "";

	if( !dir )
	{
		// Null pointer
		return false;
	}

	int dirLen = strlen(dir);
	if( dirLen > 0 )
	{
		strcpy(oldDir, dir);
		
		if( !dontSkipDelimiter && (dir[dirLen - 1] == kPathDelimiter) )
		{
			oldDir[dirLen - 1] = '\0';
			--dirLen;
		}
		
		const char* tmp = oldDir;
		const char* prev = oldDir;
		
		while( (tmp = strchr(tmp, kPathDelimiter)) != 0L )
		{
			prev = ++tmp;
		}
		
		strcpy(newDir, oldDir);
		newDir[prev - oldDir] = '\0';
		strcpy(newFile, prev);
	}
		
	uppath = newDir;
	folder = newFile;

	return true;
}

/*!
	Find a unique name for a temporary file
	\param file Return temporary file name
	\param prefix Prefix of the filename
	\param extension Extension of the filename
	\param create Set to true to create the file
	\return true on success, false otherwise
*/
bool MakeTmpFile(CStr& file, const char* prefix, const char* extension /*= 0L*/, bool create /*= false*/)
{
	static int count = 0;
	char filename[MAX_PATH];

	CStr tmpPath;
	CStr ext;
	CStr prf;

	if( extension != 0L && extension[0] != '\0' )
	{
		if( extension[0] != '.' )
			ext = ".";

		ext << extension;
	}

	if( prefix != 0L && prefix[0] != '\0' )
	{
		prf = prefix;
	}
	else
	{
		prf = "cvsgui";
	}

#ifdef WIN32
	char* ptr;
	ptr = getenv(_T("TMP"));
	if( ptr != 0L && _access(ptr, 0) != -1 )
	{
		tmpPath = ptr;
	}
	else
	{
		tmpPath = ".";
	}
#endif /* !WIN32 */
#ifdef TARGET_OS_MAC
	UFSSpec theFolder;
	OSErr err;
	
	if( (err = MacGetTempFolder(theFolder, tmpPath)) != noErr )
	{
		cvs_err("Unable to locate the preferences folder (error %d) !\n", err);
		return 0L;
	}
	
	for( ; ; ) 
	{
		sprintf(filename, "%s%d%s", (const char*)prf, count, (const char*)ext);
		if( ::strlen(filename) <= 31 )
			break;
		
		if( prf.length() == 0 ) 
		{
			cvs_err("Unable to truncate temp file !\n");
			return 0L;
		}
		
		prf.set (prf, prf.length () - 1);
	}
#endif /* !TARGET_OS_MAC */
#if qUnix
	tmpPath = "/tmp";
#endif

	struct stat sb;
	if( stat(tmpPath, &sb) == -1 || !S_ISDIR(sb.st_mode) )
	{
		cvs_err("Unable to access '%s' (error %d) !\n", (const char*)tmpPath, errno);
		return false;
	}

	if( !tmpPath.endsWith(kPathDelimiter) )
		tmpPath << kPathDelimiter;

	int maxtry = 10000;
	while( true )
	{
		sprintf(filename, "%s%d%s", (const char*)prf, count++, (const char*)ext);
		file = tmpPath;
		file << filename;

		if( stat(file, &sb) == -1 && errno == ENOENT )
		{
			if( create )
			{
				FILE* stream = fopen(file, "w");
				if( stream )
				{
					fclose(stream);
				}
			}
		
			return true;
		}

		if( maxtry-- == 0 )
		{
			cvs_err("Time out to access '%s' !\n", (const char*)tmpPath);
			break;
		}
	}

	return false;
}

/*!
	Extract the extension from the filename
	\param file File name to extract extension from
	\param base Return the base of the filename
	\param ext Return the file's extension
*/
void GetExtension(const char* file, CStr& base, CStr& ext)
{
	char* tmp = strrchr(file, '.');
	
	if( tmp == 0L )
	{
		ext = "";
		base = file;
	}
	else
	{
		ext = tmp + 1;
		base = file;
		base[tmp - file] = '\0';
	}
}

/*!
	Parse a command like 'user=who&host=cvs'
	\param cmd Command
	\param key Key
	\param value Return value
	\return	true on success, false otherwise
*/
bool GetEnvValue(const char* cmd, const char* key, UStr& value)
{
	const char* tmp = strstr(cmd, key);
	if( tmp == 0L )
		return false;

	tmp = strchr(tmp, '=');
	if( tmp == 0L )
		return false;

	tmp++;

	const char* tmp2 = strchr(tmp, '&');
	if( tmp2 == 0L )
		value = tmp;
	else
		value.set(tmp, tmp2 - tmp);

	return true;
}

/*!
	Convert a command line (possibly quoted) into regular argc, argv arguments
	\param cmdLine Command line
	\param argv Return argv arguments
	\return	The count of arguments (argc)
*/
int StringToArgv(const char* cmdLine, char** argv)
{
	int argc = 0;
	
	char Quote = '\0';				/*	Contains the quote character if we are inside "" or '', or \0 otherwise */ 
	const char*	walker1 = cmdLine;	/*	walker1 traverses the original command line */
	char* walker2;					/*	walker2 traverses the parsed copy of the command line */
	char theChar;					/*	theChar is the character we are currently parsing */
	char arguments[MAX_CMD_LEN];	/*	Will contain a parsed copy of the command line */
	
	/* Copy into arguments */
	walker2 = arguments;
	
	/* Parse command line to \0 */										
	while( (theChar = *walker1++) != '\0' ) 
	{
		/* Skip leading space before an argument */					
		if( theChar == ' ' )
			continue;
		
		/* Set argv to point to the space where we copy the next argument */
		argv [argc++] = walker2;								
		
		/* If we filled argv, skip the rest */															
		if( argc > MAX_ARGS)
		{
			argc--;
			break;
		}

		do
		{
			/* Parse an argument skip \s */
			if( theChar == '\\' && *walker1++ )
			{
				theChar = *walker1++;
			}
			else
			{
				/* When we find a quote... */
				if( theChar == '"' || theChar == '\'' )  
				{
					/* If we are not inside quotes already, */
					if( Quote == '\0' )
					{
						/* Remember we are inside quotes now */						
						Quote = theChar;
						
						/* Go to the next character */						
						continue;
					}
					
					/* If we are already inside that kind of quotes */
					if( theChar == Quote )
					{
						/* Close quotes */
						Quote = '\0';							
						
						/* Go to the next character */						
						continue;
					}
				}
			}
			
			/* Copy the character into args */
			*walker2++ = theChar;								
																
			/* Repeat until we reach the end of arguments or we reach a space that is not inside quotes */															
		}while( (*walker1 != '\0') && (((theChar = *walker1++) != ' ') || (Quote != '\0')) );
		
		/* Null-terminate the last argument */
		*walker2++ = 0;											
	}

	return argc;
}

/*!
	Launch a diff of two files
	\param f1 First file name
	\param f2 Second file name
	\return true on success, false otherwise
*/
bool LaunchDiff(const char* f1, const char* f2)
{
#if TARGET_RT_MAC_CFM
	const	OSType		kBBEditDiffCreatorCode 	= FOUR_CHAR_CODE('BBds');
	const	OSType		kResourcererCreatorCode = FOUR_CHAR_CODE('Doug');

	//	Resourcerer's Compare suite
	const	DescType	kAECompareClass = 'Comp';
	const	DescType	kAECompareEvent = 'Comp';
		const	DescType	keyNewer		= 'kNew';
		const	DescType	keyOlder		= 'kOld';
		const	DescType	keyShowDiff		= 'kShD';
		const	DescType	keyReturnDiff	= 'kRtD';
	
	OSErr				e = noErr;
	
	try {
		//	Convert to file specs
		Boolean				inShowDiffs = true;
		FSSpec				spec1 = GUSIFileSpec (f1);
		FSSpec				spec2 = GUSIFileSpec (f2);

		//	Find the file type
		FInfo	info;
		e = ::FSpGetFInfo (&spec2, &info);
		ThrowIfOSErr_(e);
		
		//	Decide on the application
		OSType				applCreator = FOUR_CHAR_CODE('????');
		switch (info.fdType) {
			case 'TEXT':
				applCreator = kBBEditDiffCreatorCode;
				break;
				
			default:
				applCreator = kResourcererCreatorCode;
				break;
			} // switch
		
		//	Try to launch it
		LaunchFlags			launchFlags = launchContinue + launchNoFileFlags;
		OSType				applType = FOUR_CHAR_CODE('APPL');
		FSSpec				applSpec;
		ProcessSerialNumber	applPSN;
		if (!UProcess::Launch (applCreator, applType, launchFlags, &applSpec, &applPSN)) Throw_(fnfErr);
		
		//	Now, diff the files.  We use the Resourcerer suite and a BBEdit glue applet.
			//	Create the parameters
		StAEDescriptor	newerDesc (spec1);
		StAEDescriptor	olderDesc (spec2);
		StAEDescriptor	showDesc (inShowDiffs);
		
			//	Create the event
		StAEDescriptor	addressDesc (typeProcessSerialNumber, &applPSN, sizeof (applPSN));
		
		StAEDescriptor	diffEvent;
		e = ::AECreateAppleEvent (kAECompareClass, kAECompareEvent, addressDesc, kAutoGenerateReturnID, kAnyTransactionID, diffEvent);
		ThrowIfOSErr_(e);
		
			//	Add the parameters
		e = ::AEPutParamDesc (diffEvent, keyNewer, newerDesc);
		ThrowIfOSErr_(e);
		e = ::AEPutParamDesc (diffEvent, keyOlder, olderDesc);
		ThrowIfOSErr_(e);
		e = ::AEPutParamDesc (diffEvent, keyShowDiff, showDesc);
		ThrowIfOSErr_(e);
		
			//	Send the event
		UAppleEventsMgr::SendAppleEvent (diffEvent);
		
		return true;
		} // try
		
	catch (LException ex) {
		} // catch
#endif

	if( gCvsPrefs.ExtDiff() == 0L )
	{
		cvs_err("The external diff program is not defined,\n");
		cvs_err("You need to set it in the preferences dialog.\n");
		return false;
	}

#ifdef WIN32
	CString args;
	CStr program, file1, file2;

	if( strchr(gCvsPrefs.ExtDiff(), ' ') != 0L )
	{
		program << '\"';
		program << gCvsPrefs.ExtDiff();
		program << '\"';
	}
	else
		program = gCvsPrefs.ExtDiff();

	if( strchr(f1, ' ') != 0L )
	{
		file1 << '\"';
		file1 << f1;
		file1 << '\"';
	}
	else
		file1 = f1;

	if( strchr(f2, ' ') != 0L )
	{
		file2 << '\"';
		file2 << f2;
		file2 << '\"';
	}
	else
		file2 = f2;

	args += file1;
	args += " ";
	args += file2;

	CWnd* pMainWnd = AfxGetMainWnd();
	HWND hParentWnd = pMainWnd ? pMainWnd->m_hWnd : NULL;

	HINSTANCE hInst = ShellExecute(hParentWnd, NULL, gCvsPrefs.ExtDiff(), args, NULL, SW_SHOWDEFAULT);

	if( (UINT)hInst < 32 )
	{
		// ExtDiff app was not found. Let's try to execute it from
		// the application path. However, this is done only when ExtDiff
		// value doesn't contain absolute path ("c:\subdir\" or "\subdir\").
		if( strchr(gCvsPrefs.ExtDiff(), ':') == 0L && (gCvsPrefs.ExtDiff()[0]) != '\\' )
		{
			CStr appPath;

			((CWincvsApp*)AfxGetApp())->GetAppPath(appPath);
			if( !appPath.endsWith(kPathDelimiter) )
				appPath << kPathDelimiter;

			appPath << gCvsPrefs.ExtDiff();
		
			hInst = ShellExecute(hParentWnd, NULL, appPath, args, NULL, SW_SHOWDEFAULT);
		}
	}

	if( (UINT)hInst < 32 )
	{
		LPVOID lpMsgBuf;
		FormatMessage(
			FORMAT_MESSAGE_ALLOCATE_BUFFER |
			FORMAT_MESSAGE_FROM_SYSTEM |
			FORMAT_MESSAGE_IGNORE_INSERTS,
			NULL,
			GetLastError(),
			MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
			(LPTSTR)&lpMsgBuf,
			0,
			NULL
			);

		cvs_err("Unable to open '%s' (%s), using console instead\n", (char*)gCvsPrefs.ExtDiff(), (LPCSTR)lpMsgBuf);
		// Free the buffer.
		LocalFree( lpMsgBuf );
		return false;
	}

	return true;
#elif qUnix
	CvsArgs args(false);
	args.add(gCvsPrefs.ExtDiff());
	args.add(f1);
	args.add(f2);
	UCvsApp::gApp->Execute(args.Argc(), args.Argv());
	return true;
#elif TARGET_RT_MAC_MACHO
	char args[MAX_PATH];
	sprintf(args, "%s '%s' '%s'", gCvsPrefs.ExtDiff(), f1, f2);
	system(args);
	return true;
#endif
}

/*!
	Launch a viewer for file
	\param fullpath Full path to the file
	\param useDefault true to use the default viewer, false to use system association
	\return true on success, false otherwise
*/
bool LaunchViewer(const char* fullpath, bool useDefault /*= false*/)
{
#ifdef WIN32
	bool done = false;
	
	if( !gCvsPrefs.UseViewerAlways() )
	{
		CStr folder, file;
		SplitPath(fullpath, folder, file);

		// MSS fix bug with files not being found by FindExecutable
		// (see Article ID: Q140724 for the problem desc of FindExecutable)
		if( !useDefault )
		{
			CWnd* pMainWnd = AfxGetMainWnd();
			HWND hParentWnd = pMainWnd ? pMainWnd->m_hWnd : NULL;

			HINSTANCE hInst = ShellExecute(hParentWnd, "open", fullpath, 0L, folder, SW_SHOWDEFAULT);
			if( (long)hInst < 32 )
			{
				// If it failed because of no valid association and file has an extension then try the "Open with..." dialog instead
				if( file.rfind('.') && 
					(SE_ERR_NOASSOC == (long)hInst || SE_ERR_ASSOCINCOMPLETE == (long)hInst) )
				{
					SHELLEXECUTEINFO seInfo = { 0 };
					seInfo.cbSize = sizeof(SHELLEXECUTEINFO);
					seInfo.hwnd = hParentWnd;
					seInfo.lpVerb = "openas";
					seInfo.lpFile = fullpath;
					seInfo.lpDirectory = folder;
					
					if( ShellExecuteEx(&seInfo) )
					{
						done = true;
					}
				}
			}
			else
			{
				done = true;
			}

			if( !done )
			{
				cvs_err("Unable to open '%s' (error %d), using default viewer instead\n", (char*)fullpath, GetLastError());
			}
		}
	}

	if( !done )
	{
		if( gCvsPrefs.Viewer() == 0L )
		{
			cvs_err("You need to define a default viewer for this file in the preferences\n");
			return false;
		}

		const char* argv[3] = {0L, 0L, 0L};
		CStr program, file;
		if( strchr(gCvsPrefs.Viewer(), ' ') != 0L )
		{
			program << '\"';
			program << gCvsPrefs.Viewer();
			program << '\"';
		}
		else
			program = gCvsPrefs.Viewer();
		
		if( strchr(fullpath, ' ') != 0L )
		{
			file << '\"';
			file << fullpath;
			file << '\"';
		}
		else
			file = fullpath;

		argv[0] = program;
		argv[1] = file;
		
		int process = _spawnvp(_P_NOWAIT, gCvsPrefs.Viewer(), argv);
	}

	return true;
#endif /* WIN32 */
	
#ifdef qMacCvsPP
	return true;
#endif /* qMacCvsPP */
	
#if qUnix
	return true;
#endif // qUnix
}

/*!
	Launch an editor for a file
	\param tmpFile File name
	\param par Return data parameter to return the info about running editor process
	\return true on success, false otherwise
*/
bool LaunchEditor(const char* tmpFile, void* par /*= 0L*/)
{
	CStr program, file;

	if( strchr(gCvsPrefs.Viewer(), ' ') != 0L )
	{
		program << '\"';
		program << gCvsPrefs.Viewer();
		program << '\"';
	}
	else
		program = gCvsPrefs.Viewer();

	if( strchr(tmpFile, ' ') != 0L )
	{
		file << '\"';
		file << tmpFile;
		file << '\"';
	}
	else
		file = tmpFile;

	CStr command;
	command << program;
	command << " ";
	command << file;

#ifdef WIN32
	STARTUPINFO sStartupInfo;
	PROCESS_INFORMATION sProcessInformation;

	// Execute the command with a call to the CreateProcess API call.
	memset( &sStartupInfo, 0, sizeof(sStartupInfo) );
	sStartupInfo.cb = sizeof( sStartupInfo );
	sStartupInfo.dwFlags = STARTF_USESHOWWINDOW;
	sStartupInfo.wShowWindow = SW_SHOWNORMAL;
	
	if( !CreateProcess( NULL,
		(char*)command,
		NULL,
		NULL,
		FALSE,
		0,
		NULL,
		NULL, 
		&sStartupInfo,
		&sProcessInformation) )
	{
		sProcessInformation.hProcess = INVALID_HANDLE_VALUE;
		sProcessInformation.hThread = INVALID_HANDLE_VALUE;
	}
	
	*((PROCESS_INFORMATION*)par) = sProcessInformation;
	return sProcessInformation.hProcess != INVALID_HANDLE_VALUE;
#endif /* WIN32 */

#ifdef qMacCvsPP
	return true;
#endif /* qMacCvsPP */
	
#if qUnix
	return true;
#endif // qUnix
}

#ifdef WIN32
/*!
	Removes folder separators by replacing them with dots so that temporary file 
	could be created without creating matching subfolders in the temporary file folder
	\param tmpf File with subfolder
	\return true on success, false otherwise
	\note Windows client may provide file names pointing into subfolders
*/
static void FlatenSubFolders(CStr& tmpf)
{
	char* cp = (char*)tmpf;
	
	if( cp )
	{
		while( *cp )
		{
			if( *cp == kPathDelimiter )
			{
				*cp = '.';
			}

			cp++;
		}
	}	
}
#elif TARGET_OS_MAC
static void FlatenSubFolders(CStr& tmpf)
{
	char *tmp;
	if((tmp = strrchr(tmpf, kPathDelimiter)) != NULL)
	{
		UStr newStr(++tmp);
		tmpf = newStr;
	}
}
#else
#define FlatenSubFolders(tmpf) // no-op on other platforms
#endif

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

/*!
	Verify the selection hadler is properly setup to carry on launching command
	\param handler Command handler to be tested
	\param mf Returned selection as MultiFiles
	\return true if handler is properly setup, false otherwise
*/
static bool CheckSelectionHandler(KiSelectionHandler& handler, MultiFiles*& mf)
{
	if( gCvsPrefs.empty() )
	{
		return false;
	}

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

	return true;
}

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

	if( !CompatGetInit(forceCvsroot) )
		return;

	CvsArgs args;

	if( forceCvsroot )
	{
		args.addcvsroot();
	}

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

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

	if( !CompatGetLogin(forceCvsroot) )
		return;

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

	CvsArgs args;

	if( forceCvsroot )
	{
		args.addcvsroot();
	}

	args.add("login");

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

/*!
	Execute <b>cvs logou</b> command
*/
void CvsCmdLogout(void)
{
	if( gCvsPrefs.empty() )
		return;
	
	AuthenModel* curModel = AuthenModel::GetInstance(gAuthen.kind());
	if( curModel->HasLogin() )
	{
		cvs_err("Logging out is required for pserver or sspi authentication only (see Preferences dialog).\n"
			"Please consult the CVS manual for more details.\n");
	}
	
	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
*/
void CvsCmdRtagCreate(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool norecurs;
	bool useMostRecent;
	bool overwriteExisting;
	bool lookAttic;
	CPStr modname;
	CPStr tagname;
	CPStr date;
	CPStr rev;

	if( !CompatRtagCreate(mf, norecurs, overwriteExisting, tagname, modname, date, rev, useMostRecent, lookAttic) )
		return;

	CvsArgs args;
	args.add("rtag");
	
	if( norecurs )
		args.add("-l");
	
	if( useMostRecent )
		args.add("-f");
	
	if( overwriteExisting )
		args.add("-F");
	
	if( lookAttic )
		args.add("-a");
	
	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 rtag -d</b> command
	\param handler Selection for the command
*/
void CvsCmdRtagDelete(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool norecurs;
	bool lookAttic;
	CPStr modname;
	CPStr tagname;

	if( !CompatRtagDelete(mf, norecurs, lookAttic, tagname, modname) )
			return;

	CvsArgs args;
	args.add("rtag");
	args.add("-d");
	
	if( norecurs )
		args.add("-l");
	
	if( lookAttic )
		args.add("-a");

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

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

	bool norecurs;
	bool useMostRecent;
	bool lookAttic;
	CPStr modname;
	CPStr branchname;
	CPStr date;
	CPStr rev;

	if( !CompatRtagBranch(mf, norecurs, branchname, modname, date, rev, useMostRecent, lookAttic) )
			return;

	CvsArgs args;
	args.add("rtag");
	args.add("-b");
	
	if( norecurs )
		args.add("-l");
	
	if( useMostRecent )
		args.add("-f");
	
	if( lookAttic )
		args.add("-a");
	
	if( !date.empty() )
	{
		args.add("-D");
		args.add(date);
	}
	
	if( !rev.empty() )
	{
		args.add("-r");
		args.add(rev);
	}

	args.add(branchname);
	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;
	CPStr modname;
	CPStr path(dir);
	bool toStdout;
	CPStr date;
	CPStr rev;
	bool useMostRecent;
	CPStr rev1;
	CPStr rev2;
	bool doexport;
	bool forceCvsroot;
	bool overrideCheckoutDir;
	CPStr checkoutDir;

	if( !CompatGetCheckout(mf, modname, path, norecurs,
		toStdout, date, rev, useMostRecent, rev1, rev2, doexport, forceCvsroot,
		overrideCheckoutDir, checkoutDir) )
	{
		return;
	}

	CvsArgs args;

	if( forceCvsroot )
	{
		args.addcvsroot();
	}

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

	if( gCvsPrefs.PruneOption() && !doexport )
		args.add("-P");
	
	if( norecurs )
		args.add("-l");
	
	if( toStdout )
		args.add("-p");
	
	if( useMostRecent )
		args.add("-f");
	
	if( !rev1.empty() )
	{
		CPStr tmp;
		tmp = "-j";
		tmp << rev1;
		args.add(tmp);
	}
	
	if( !rev2.empty() )
	{
		CPStr tmp;
		tmp = "-j";
		tmp << rev2;
		args.add(tmp);
	}
	
	if( !date.empty() )
	{
		args.add("-D");
		args.add(date);
	}
	
	if( !rev.empty() )
	{
		args.add("-r");
		args.add(rev);
	}

	if( doexport && rev.empty() && date.empty() )
	{
		args.add("-r");
		args.add("HEAD");
	}

	if( overrideCheckoutDir )
	{
		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)
	{
		ReportConflict* entries;
		ReportWarning* warnings;
		const char* dir = mfi->getdir();
		
		if( !CompatImportFilter(dir, entries, warnings) )
			return;
		
		CPStr modname, vendortag, reltag, path(dir);
		CStr logmsg;
		bool useDefIgnore;
		bool useFilesTime;
		
		if( !CompatGetImport(mf, modname, logmsg, vendortag, reltag, path, useDefIgnore, useFilesTime) )
			return;
		
		CvsArgs args;
		args.add("import");
		
		if( useFilesTime )
		{
			args.add("-d");
		}
		
		// 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);
		
		args.add("-m");
		args.add(CleanupLogMsg(logmsg));
		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;

	CvsArgs args(false);
	const char* dir;

	if( !CompatGetCommand(mf, args, dir) )
		return;

	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;
	CPStr date;
	CPStr rev;
	bool useMostRecent;
	CPStr rev1;
	CPStr rev2;
	bool createMissDir;
	bool getCleanCopy;

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

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

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

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

		if( !queryOnly )
		{
			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() )
			{
				CPStr tmp;
				tmp = "-j";
				tmp << rev1;
				args.add(tmp);
			}
			
			if( !rev2.empty() )
			{
				CPStr 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 firstTime = true;
	bool norecurs;
	CStr logmsg;
	bool forceCommit;
	bool forceRecurse;
	CPStr rev;
	bool noModuleProgram;
	bool checkValidEdits;	

	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		if( firstTime )
		{
			// we have to give the path where the commit is done is
			// the user expects to fill the CVS/Template file. we just
			// grab the first one for this purpose.
			CStr pathTmpl;
			pathTmpl = mfi->getdir();

			if( !CompatGetCommit(mf, logmsg, norecurs, forceCommit, forceRecurse, rev, noModuleProgram, checkValidEdits, pathTmpl) )
				return;

			firstTime = false;
		}

		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
*/
void CvsCmdDiff(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool noRecurs;
	bool isDate1;
	bool isDate2;
	bool useExtDiff;
	bool useUnifiedDiff;
	CPStr rev1;
	CPStr rev2;
	bool noWhiteSpace;
	bool noBlankLines;
	bool ignoreCase;
	bool extDiffResult;

	if( !CompatGetDiff(mf, noRecurs, isDate1, isDate2, rev1, rev2, useExtDiff, useUnifiedDiff, noWhiteSpace, noBlankLines, ignoreCase, 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;

				// 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 TARGET_RT_MAC_CFM
				decodeCVS (file1);
#endif

				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 TARGET_RT_MAC_CFM
					decodeCVS (file2);
#endif
				}

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

				extDiffResult = LaunchDiff(file1, file2);
			}
		}

		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( useUnifiedDiff )
				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

			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);
			
			if( !args->HasProblem() ||
				!CvsAlert("Some problems have been found while adding files and should be corrected", "Cancel", "Ignore") )
			{
				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("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;
	CPStr date;
	bool headerOnly;
	bool noTags;
	bool onlyRCSfile;
	bool hasRev;
	CPStr rev;
	CPStr states;
	bool hasUser;
	CPStr users;

	if( !CompatGetLog(mf, noRecurs, defBranch, date,
		headerOnly, 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( headerOnly )
			args.add("-h");

		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 )
		{
			CPStr tmp;
			tmp = "-r";
			tmp << rev;
			args.add(tmp);
		}

		if( hasUser )
		{
			CPStr 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
*/
void CvsCmdTagCreate(KiSelectionHandler& handler)
{	
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool noRecurs, overwriteExisting, checkUnmod;
	CPStr tagName;

	if( !CompatTagCreate(mf, noRecurs, overwriteExisting, tagName, checkUnmod) )
		return;
	
	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("tag");

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

		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 tag -d</b> command
	\param handler Selection for the command
*/
void CvsCmdTagDelete(KiSelectionHandler& handler)
{	
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

	bool noRecurs;
	CPStr tagName;

	if( !CompatTagDelete(mf, noRecurs, tagName) )
		return;
	
	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("tag");
		args.add("-d");
		
		if( noRecurs )
			args.add("-l");

		args.add(tagName);

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

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

	bool noRecurs, checkUnmod;
	CPStr branchName;

	if( !CompatTagBranch(mf, noRecurs, branchName, checkUnmod) )
		return;
	
	MultiFiles::const_iterator mfi;
	for(mfi = mf->begin(); mfi != mf->end(); ++mfi)
	{
		CvsArgs args;
		args.add("tag");
		args.add("-b");

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

		if( checkUnmod )
			args.add("-c");

		args.add(branchName);

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

/*!
	Execute <b>cvs </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("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 = ".";
	}

	bool removeDirectory = false;

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

	CvsArgs args;
	args.add("release");
	
	if( removeDirectory )
		args.add("-d");
	
	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
*/
void CvsCmdAnnotate(KiSelectionHandler& handler)
{
	MultiFiles* mf = NULL;
	if( !CheckSelectionHandler(handler, mf) )
		return;

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

	if( !CompatGetAnnotate(mf, 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);
#ifdef qMacCvsPP
			decodeCVS (file);
#endif

			if( file.empty() )
				continue;

			LaunchViewer(file, true);
		}
	}
}

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

	if( !CvsAlert("Are you sure you want to erase selected files?", "Yes", "No") )
	{
		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;
					errText << "Unable to remove '" << fn << "' (error " << errno << ")\n";

					cvs_err( errText.c_str() );
				}
				else
				{
					UStr errText;
#if TARGET_OS_MAC
					errText << "'" << fn << "' has been moved successfully to the trash...\n";
#else
					errText << "'" << fn << "' has been moved successfully to the recycle bin ...\n";
#endif
					cvs_out( errText.c_str() );
				}
			}
		}
	}
	
	CompatEndMoveToTrash();
}
