/*
 * Copyright (c) 2002-2004 Christian Loose <christian.loose@kdemail.net>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */

#include "cvsservice.h"

#include <tqintdict.h>
#include <tqstring.h>

#include <dcopref.h>
#include <dcopclient.h>
#include <tdeapplication.h>
#include <tdeconfig.h>
#include <tdelocale.h>
#include <tdemessagebox.h>
#include <kprocess.h>

#include "cvsjob.h"
#include "cvsloginjob.h"
#include "cvsserviceutils.h"
#include "repository.h"
#include "sshagent.h"


static const char SINGLE_JOB_ID[]   = "NonConcurrentJob";
static const char REDIRECT_STDERR[] = "2>&1";

enum WatchEvents { None=0, All=1, Commits=2, Edits=4, Unedits=8 };

struct CvsService::Private
{
    Private() : singleCvsJob(0), lastJobId(0), repository(0) {}
    ~Private()
    {
        delete repository;
        delete singleCvsJob;
    }

    CvsJob*               singleCvsJob;   // non-concurrent cvs job, like update or commit
    DCOPRef               singleJobRef;   // DCOP reference to non-concurrent cvs job
    TQIntDict<CvsJob>      cvsJobs;        // concurrent cvs jobs, like diff or annotate
    TQIntDict<CvsLoginJob> loginJobs;
    unsigned              lastJobId;

    TQCString              appId;          // cache the DCOP clients app id

    Repository*           repository;

    CvsJob* createCvsJob();
    DCOPRef setupNonConcurrentJob(Repository* repo = 0);

    bool hasWorkingCopy();
    bool hasRunningJob();
};


CvsService::CvsService()
    : DCOPObject("CvsService")
    , d(new Private)
{
    d->appId = kapp->dcopClient()->appId();

    // create non-concurrent cvs job
    d->singleCvsJob = new CvsJob(SINGLE_JOB_ID);
    d->singleJobRef.setRef(d->appId, d->singleCvsJob->objId());

    // create repository manager
    d->repository = new Repository();

    d->cvsJobs.setAutoDelete(true);
    d->loginJobs.setAutoDelete(true);

    TDEConfig* config = kapp->config();
    TDEConfigGroupSaver cs(config, "General");
    if( config->readBoolEntry("UseSshAgent", false) )
    {
        // use the existing or start a new ssh-agent
        SshAgent ssh;
        // TODO CL do we need the return value?
        //bool res = ssh.querySshAgent();
        ssh.querySshAgent();
    }
}


CvsService::~CvsService()
{
    // kill the ssh-agent (when we started it)
    SshAgent ssh;
    ssh.killSshAgent();

    d->cvsJobs.clear();
    d->loginJobs.clear();
    delete d;
}


DCOPRef CvsService::add(const TQStringList& files, bool isBinary)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs add [-kb] [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "add";

    if( isBinary )
        *d->singleCvsJob << "-kb";

    *d->singleCvsJob << CvsServiceUtils::joinFileList(files) << REDIRECT_STDERR;

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::addWatch(const TQStringList& files, int events)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "watch add";

    if( events != All )
    {
        if( events & Commits )
            *d->singleCvsJob << "-a commit";
        if( events & Edits )
            *d->singleCvsJob << "-a edit";
        if( events & Unedits )
            *d->singleCvsJob << "-a unedit";
    }

    *d->singleCvsJob << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::annotate(const TQString& fileName, const TQString& revision)
{
    if( !d->hasWorkingCopy() )
        return DCOPRef();

    // create a cvs job
    CvsJob* job = d->createCvsJob();

    // assemble the command line
    // (cvs log [FILE] && cvs annotate [-r rev] [FILE])
    TQString quotedName = TDEProcess::quote(fileName);
    TQString cvsClient  = d->repository->cvsClient();

    *job << "(" << cvsClient << "log" << quotedName << "&&"
         << cvsClient << "annotate";

    if( !revision.isEmpty() )
        *job << "-r" << revision;

    // *Hack*
    // because the string "Annotations for blabla" is
    // printed to stderr even with option -Q.
    *job << quotedName << ")" << REDIRECT_STDERR;

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::checkout(const TQString& workingDir, const TQString& repository,
                             const TQString& module, const TQString& tag, 
                             bool pruneDirs)
{
    if( d->hasRunningJob() )
        return DCOPRef();

    Repository repo(repository);

    // assemble the command line
    // cd [DIRECTORY] && cvs -d [REPOSITORY] checkout [-r tag] [-P] [MODULE]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << "cd" << TDEProcess::quote(workingDir) << "&&"
                     << repo.cvsClient()
                     << "-d" << repository
                     << "checkout";

    if( !tag.isEmpty() )
        *d->singleCvsJob << "-r" << tag;

    if( pruneDirs )
        *d->singleCvsJob << "-P";

    *d->singleCvsJob << module;

    return d->setupNonConcurrentJob(&repo);
}


DCOPRef CvsService::checkout(const TQString& workingDir, const TQString& repository,
                             const TQString& module, const TQString& tag, 
                             bool pruneDirs, const TQString& alias, bool exportOnly)
{
    if( d->hasRunningJob() )
        return DCOPRef();

    Repository repo(repository);

    // assemble the command line
    // cd [DIRECTORY] && cvs -d [REPOSITORY] co [-r tag] [-P] [-d alias] [MODULE]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << "cd" << TDEProcess::quote(workingDir) << "&&"
                     << repo.cvsClient()
                     << "-d" << repository;
    if( exportOnly)
      *d->singleCvsJob << "export";
    else
      *d->singleCvsJob << "checkout";

    if( !tag.isEmpty() )
        *d->singleCvsJob << "-r" << tag;

    if( pruneDirs && !exportOnly )
        *d->singleCvsJob << "-P";

    if( !alias.isEmpty() )
      *d->singleCvsJob << "-d" << alias;

    *d->singleCvsJob << module;

    return d->setupNonConcurrentJob(&repo);
}

DCOPRef CvsService::checkout(const TQString& workingDir, const TQString& repository,
                             const TQString& module, const TQString& tag, 
                             bool pruneDirs, const TQString& alias, bool exportOnly,
                             bool recursive)
{
    if( d->hasRunningJob() )
        return DCOPRef();

    Repository repo(repository);

    // assemble the command line
    // cd [DIRECTORY] && cvs -d [REPOSITORY] co [-r tag] [-P] [-d alias] [MODULE]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << "cd" << TDEProcess::quote(workingDir) << "&&"
                     << repo.cvsClient()
                     << "-d" << repository;
    if( exportOnly)
      *d->singleCvsJob << "export";
    else
      *d->singleCvsJob << "checkout";

    if( !tag.isEmpty() )
        *d->singleCvsJob << "-r" << tag;

    if( pruneDirs && !exportOnly )
        *d->singleCvsJob << "-P";

    if( !alias.isEmpty() )
      *d->singleCvsJob << "-d" << alias;

    if( ! recursive )
        *d->singleCvsJob << "-l";

    *d->singleCvsJob << module;

    return d->setupNonConcurrentJob(&repo);
}

DCOPRef CvsService::commit(const TQStringList& files, const TQString& commitMessage,
                           bool recursive)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs commit [-l] [-m MESSAGE] [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "commit";

    if( !recursive )
        *d->singleCvsJob << "-l";

    *d->singleCvsJob << "-m" << TDEProcess::quote(commitMessage)
                     << CvsServiceUtils::joinFileList(files) << REDIRECT_STDERR;

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::createRepository(const TQString& repository)
{
    if( d->hasRunningJob() )
        return DCOPRef();
        
    // assemble the command line
    // cvs -d [REPOSITORY] init
    d->singleCvsJob->clearCvsCommand();
    
    *d->singleCvsJob << "mkdir -p" << TDEProcess::quote(repository) << "&&"
                     << d->repository->cvsClient() 
                     << "-d" << TDEProcess::quote(repository)
                     << "init";

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::createTag(const TQStringList& files, const TQString& tag,
                              bool branch, bool force)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs tag [-b] [-F] [TAG] [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "tag";

    if( branch )
        *d->singleCvsJob << "-b";

    if( force )
        *d->singleCvsJob << "-F";

    *d->singleCvsJob << TDEProcess::quote(tag)
                     << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::deleteTag(const TQStringList& files, const TQString& tag,
                              bool branch, bool force)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs tag -d [-b] [-F] [TAG] [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "tag" << "-d";

    if( branch )
        *d->singleCvsJob << "-b";

    if( force )
        *d->singleCvsJob << "-F";

    *d->singleCvsJob << TDEProcess::quote(tag)
                     << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::downloadCvsIgnoreFile(const TQString& repository,
                                          const TQString& outputFile)
{
    Repository repo(repository);

    // create a cvs job
    CvsJob* job = d->createCvsJob();

    // assemble the command line
    // cvs -d [REPOSITORY] -q checkout -p CVSROOT/cvsignore > [OUTPUTFILE]
    *job << repo.cvsClient() << "-d" << repository 
         << "-q checkout -p CVSROOT/cvsignore >" 
         << TDEProcess::quote(outputFile);

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::downloadRevision(const TQString& fileName,
                                     const TQString& revision,
                                     const TQString& outputFile)
{
    if( !d->hasWorkingCopy() )
        return DCOPRef();

    // create a cvs job
    CvsJob* job = d->createCvsJob();

    // assemble the command line
    // cvs update -p -r [REV] [FILE] > [OUTPUTFILE]
    *job << d->repository->cvsClient() << "update -p";

    if( !revision.isEmpty() )
        *job << "-r" << TDEProcess::quote(revision);

    *job << TDEProcess::quote(fileName) << ">" << TDEProcess::quote(outputFile);

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::downloadRevision(const TQString& fileName,
                                     const TQString& revA,
                                     const TQString& outputFileA,
                                     const TQString& revB,
                                     const TQString& outputFileB)
{
    if( !d->hasWorkingCopy() )
        return DCOPRef();

    // create a cvs job
    CvsJob* job = d->createCvsJob();

    // assemble the command line
    // cvs update -p -r [REVA] [FILE] > [OUTPUTFILEA] ;
    // cvs update -p -r [REVB] [FILE] > [OUTPUTFILEB]
    *job << d->repository->cvsClient() << "update -p"
         << "-r" << TDEProcess::quote(revA)
         << TDEProcess::quote(fileName) << ">" << TDEProcess::quote(outputFileA)
         << ";" << d->repository->cvsClient() << "update -p"
         << "-r" << TDEProcess::quote(revB)
         << TDEProcess::quote(fileName) << ">" << TDEProcess::quote(outputFileB);

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::diff(const TQString& fileName, const TQString& revA,
                         const TQString& revB, const TQString& diffOptions,
                         unsigned contextLines)
{
    // cvs diff [DIFFOPTIONS] -U CONTEXTLINES [-r REVA] {-r REVB] [FILE]
    TQString format = "-U" + TQString::number(contextLines);
    return diff(fileName, revA, revB, diffOptions, format);
}


DCOPRef CvsService::diff(const TQString& fileName, const TQString& revA,
                         const TQString& revB, const TQString& diffOptions,
                         const TQString& format)
{
    if( !d->hasWorkingCopy() )
        return DCOPRef();

    // create a cvs job
    CvsJob* job = d->createCvsJob();

    // assemble the command line
    // cvs diff [DIFFOPTIONS] [FORMAT] [-r REVA] {-r REVB] [FILE]
    *job << d->repository->cvsClient() << "diff" << diffOptions
         << format;

    if( !revA.isEmpty() )
        *job << "-r" << TDEProcess::quote(revA);

    if( !revB.isEmpty() )
        *job << "-r" << TDEProcess::quote(revB);

    *job << TDEProcess::quote(fileName);

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::edit(const TQStringList& files)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs edit [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "edit"
                     << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::editors(const TQStringList& files)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs editors [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "editors"
                     << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::history()
{
    if( !d->hasWorkingCopy() )
        return DCOPRef();

    // create a cvs job
    CvsJob* job = d->createCvsJob();

    // assemble the command line
    // cvs history -e -a
    *job << d->repository->cvsClient() << "history -e -a";

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::import(const TQString& workingDir, const TQString& repository,
                           const TQString& module, const TQString& ignoreList,
                           const TQString& comment, const TQString& vendorTag,
                           const TQString& releaseTag, bool importAsBinary)
{
    if( d->hasRunningJob() )
        return DCOPRef();

    Repository repo(repository);

    // assemble the command line
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << "cd" << TDEProcess::quote(workingDir) << "&&"
                     << repo.cvsClient()
                     << "-d" << repository
                     << "import";

    if( importAsBinary )
        *d->singleCvsJob << "-kb";
        
    const TQString ignore = ignoreList.stripWhiteSpace();
    if( !ignore.isEmpty() )
        *d->singleCvsJob << "-I" << TDEProcess::quote(ignore);

    TQString logMessage = comment.stripWhiteSpace();
    logMessage.prepend("\"");
    logMessage.append("\"");
    *d->singleCvsJob << "-m" << logMessage;

    *d->singleCvsJob << module << vendorTag << releaseTag;

    return d->setupNonConcurrentJob(&repo);
}


DCOPRef CvsService::import(const TQString& workingDir, const TQString& repository,
                           const TQString& module, const TQString& ignoreList,
                           const TQString& comment, const TQString& vendorTag,
                           const TQString& releaseTag, bool importAsBinary,
                           bool useModificationTime)
{
    if( d->hasRunningJob() )
        return DCOPRef();

    Repository repo(repository);

    // assemble the command line
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << "cd" << TDEProcess::quote(workingDir) << "&&"
                     << repo.cvsClient()
                     << "-d" << repository
                     << "import";

    if( importAsBinary )
        *d->singleCvsJob << "-kb";
        
    if( useModificationTime )
        *d->singleCvsJob << "-d";

    const TQString ignore = ignoreList.stripWhiteSpace();
    if( !ignore.isEmpty() )
        *d->singleCvsJob << "-I" << TDEProcess::quote(ignore);

    TQString logMessage = comment.stripWhiteSpace();
    logMessage.prepend("\"");
    logMessage.append("\"");
    *d->singleCvsJob << "-m" << logMessage;

    *d->singleCvsJob << module << vendorTag << releaseTag;

    return d->setupNonConcurrentJob(&repo);
}


DCOPRef CvsService::lock(const TQStringList& files)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs admin -l [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "admin -l"
                     << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::log(const TQString& fileName)
{
    if( !d->hasWorkingCopy() )
        return DCOPRef();

    // create a cvs job
    CvsJob* job = d->createCvsJob();

    // assemble the command line
    // cvs log [FILE]
    *job << d->repository->cvsClient() << "log" << TDEProcess::quote(fileName);

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::login(const TQString& repository)
{
    if( repository.isEmpty() )
        return DCOPRef();

    Repository repo(repository);

    // create a cvs job
    ++(d->lastJobId);

    CvsLoginJob* job = new CvsLoginJob(d->lastJobId);
    d->loginJobs.insert(d->lastJobId, job);

    // TODO: CVS_SERVER doesn't work ATM
//    job->setServer(repo.server());

    // assemble the command line
    // cvs -d [REPOSITORY] login
    job->setCvsClient(repo.clientOnly().local8Bit());
    job->setRepository(repository.local8Bit());

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::logout(const TQString& repository)
{
    if( repository.isEmpty() )
        return DCOPRef();

    Repository repo(repository);

    // create a cvs job
    ++(d->lastJobId);

    CvsJob* job = new CvsJob(d->lastJobId);
    d->cvsJobs.insert(d->lastJobId, job);

    job->setRSH(repo.rsh());
    job->setServer(repo.server());
    job->setDirectory(repo.workingCopy());

    // assemble the command line
    // cvs -d [REPOSITORY] logout
    *job << repo.cvsClient() << "-d" << repository << "logout";

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::makePatch()
{
    return makePatch("", "-u");
}


DCOPRef CvsService::makePatch(const TQString& diffOptions, const TQString& format)
{
    if( !d->hasWorkingCopy() )
        return DCOPRef();

    // create a cvs job
    CvsJob* job = d->createCvsJob();

    // assemble the command line
    // cvs diff [DIFFOPTIONS] [FORMAT] -R 2>/dev/null
    *job << d->repository->cvsClient() << "diff" << diffOptions << format << "-R"
         << "2>/dev/null";

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::moduleList(const TQString& repository)
{
    Repository repo(repository);

    // create a cvs job
    ++(d->lastJobId);

    CvsJob* job = new CvsJob(d->lastJobId);
    d->cvsJobs.insert(d->lastJobId, job);

    job->setRSH(repo.rsh());
    job->setServer(repo.server());
    job->setDirectory(repo.workingCopy());

    // assemble the command line
    // cvs -d [REPOSITORY] checkout -c
    *job << repo.cvsClient() << "-d" << repository << "checkout -c";

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::remove(const TQStringList& files, bool recursive)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs remove -f [-l] [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "remove -f";

    if( !recursive )
        *d->singleCvsJob << "-l";

    *d->singleCvsJob << CvsServiceUtils::joinFileList(files) << REDIRECT_STDERR;

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::removeWatch(const TQStringList& files, int events)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "watch remove";

    if( events != All )
    {
        if( events & Commits )
            *d->singleCvsJob << "-a commit";
        if( events & Edits )
            *d->singleCvsJob << "-a edit";
        if( events & Unedits )
            *d->singleCvsJob << "-a unedit";
    }

    *d->singleCvsJob << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::rlog(const TQString& repository, const TQString& module, 
                         bool recursive)
{
    Repository repo(repository);

    // create a cvs job
    ++(d->lastJobId);

    CvsJob* job = new CvsJob(d->lastJobId);
    d->cvsJobs.insert(d->lastJobId, job);

    job->setRSH(repo.rsh());
    job->setServer(repo.server());

    // assemble the command line
    // cvs -d [REPOSITORY] rlog [-l] [MODULE]
    *job << repo.cvsClient() << "-d" << repository << "rlog";

    if( !recursive )
        *job << "-l";

    *job << module;

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::simulateUpdate(const TQStringList& files, bool recursive,
                                   bool createDirs, bool pruneDirs)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs -n update [-l] [-d] [-P] [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "-n -q update";

    if( !recursive )
        *d->singleCvsJob << "-l";

    if( createDirs )
        *d->singleCvsJob << "-d";

    if( pruneDirs )
        *d->singleCvsJob << "-P";

    *d->singleCvsJob << CvsServiceUtils::joinFileList(files) << REDIRECT_STDERR;

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::status(const TQStringList& files, bool recursive, bool tagInfo)
{
    if( !d->hasWorkingCopy() )
        return DCOPRef();

    // create a cvs job
    CvsJob* job = d->createCvsJob();

    // assemble the command line
    // cvs status [-l] [-v] [FILES]
    *job << d->repository->cvsClient() << "status";

    if( !recursive )
        *job << "-l";

    if( tagInfo )
        *job << "-v";

    *job << CvsServiceUtils::joinFileList(files);

    // return a DCOP reference to the cvs job
    return DCOPRef(d->appId, job->objId());
}


DCOPRef CvsService::unedit(const TQStringList& files)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // echo y | cvs unedit [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << "echo y |"
                     << d->repository->cvsClient() << "unedit"
                     << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::unlock(const TQStringList& files)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs admin -u [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "admin -u"
                     << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::update(const TQStringList& files, bool recursive,
                           bool createDirs, bool pruneDirs, const TQString& extraOpt)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs update [-l] [-d] [-P] [EXTRAOPTIONS] [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "-q update";

    if( !recursive )
        *d->singleCvsJob << "-l";

    if( createDirs )
        *d->singleCvsJob << "-d";

    if( pruneDirs )
        *d->singleCvsJob << "-P";

    *d->singleCvsJob << extraOpt << CvsServiceUtils::joinFileList(files)
                     << REDIRECT_STDERR;

    return d->setupNonConcurrentJob();
}


DCOPRef CvsService::watchers(const TQStringList& files)
{
    if( !d->hasWorkingCopy() || d->hasRunningJob() )
        return DCOPRef();

    // assemble the command line
    // cvs watchers [FILES]
    d->singleCvsJob->clearCvsCommand();

    *d->singleCvsJob << d->repository->cvsClient() << "watchers"
                     << CvsServiceUtils::joinFileList(files);

    return d->setupNonConcurrentJob();
}


void CvsService::quit()
{
    kapp->quit();
}


CvsJob* CvsService::Private::createCvsJob()
{
    ++lastJobId;

    // create a cvs job
    CvsJob* job = new CvsJob(lastJobId);
    cvsJobs.insert(lastJobId, job);

    job->setRSH(repository->rsh());
    job->setServer(repository->server());
    job->setDirectory(repository->workingCopy());

    return job;
}


DCOPRef CvsService::Private::setupNonConcurrentJob(Repository* repo)
{
    // no explicit repository provided?
    if( !repo )
        repo = repository;
        
    singleCvsJob->setRSH(repo->rsh());
    singleCvsJob->setServer(repo->server());
    singleCvsJob->setDirectory(repo->workingCopy());

    return singleJobRef;
}


bool CvsService::Private::hasWorkingCopy()
{
    if( repository->workingCopy().isEmpty() )
    {
        KMessageBox::sorry(0, i18n("You have to set a local working copy "
                                   "directory before you can use this function!"));
        return false;
    }

    return true;
}


bool CvsService::Private::hasRunningJob()
{
    bool result = singleCvsJob->isRunning();

    if( result )
        KMessageBox::sorry(0, i18n("There is already a job running"));

    return result;
}