You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
17 KiB
543 lines
17 KiB
/* ****************************************************************************
|
|
This file is part of KBabel
|
|
|
|
Copyright (C) 2002-2003 by Marco Wegner <mail@marcowegner.de>
|
|
Copyright (C) 2005, 2006 by Nicolas GOUTTE <goutte@kde.org>
|
|
|
|
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 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 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
In addition, as a special exception, the copyright holders give
|
|
permission to link the code of this program with any edition of
|
|
the TQt library by Trolltech AS, Norway (or with modified versions
|
|
of TQt that use the same license as TQt), and distribute linked
|
|
combinations including the two. You must obey the GNU General
|
|
Public License in all respects for all of the code used other than
|
|
TQt. If you modify this file, you may extend this exception to
|
|
your version of the file, but you are not obligated to do so. If
|
|
you do not wish to do so, delete this exception statement from
|
|
your version.
|
|
|
|
**************************************************************************** */
|
|
|
|
|
|
// System include files
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
// TQt include files
|
|
#include <tqdir.h>
|
|
#include <tqfile.h>
|
|
#include <tqfileinfo.h>
|
|
#include <tqregexp.h>
|
|
#include <tqstring.h>
|
|
#include <tqstringlist.h>
|
|
#include <tqdom.h>
|
|
// KDE include files
|
|
#include <tdeapplication.h>
|
|
#include <tdelocale.h>
|
|
#include <tdemessagebox.h>
|
|
#include <tdetempfile.h>
|
|
#include <kdebug.h>
|
|
#include <tdeprocess.h>
|
|
// project specific include files
|
|
#include "svnhandler.h"
|
|
|
|
SVNHandler::SVNHandler( const TQString& poBaseDir, const TQString& potBaseDir )
|
|
{
|
|
setPOBaseDir( poBaseDir );
|
|
setPOTBaseDir( potBaseDir );
|
|
_autoUpdateTemplates = false;
|
|
}
|
|
|
|
void SVNHandler::setPOBaseDir( const TQString& dir )
|
|
{
|
|
// check if '.svn/entries' exists in the PO base directory
|
|
if ( TQFileInfo( dir + "/.svn/entries" ).exists( ) ) {
|
|
_isPORepository = true;
|
|
_poBaseDir = dir;
|
|
} else
|
|
_isPORepository = false;
|
|
emit signalIsPORepository( _isPORepository );
|
|
}
|
|
|
|
void SVNHandler::setPOTBaseDir( const TQString& dir )
|
|
{
|
|
// check if '.svn/entries' exists in the POT base directory
|
|
if ( TQFileInfo( dir + "/.svn/entries" ).exists( ) ) {
|
|
_isPOTRepository = true;
|
|
_potBaseDir = dir;
|
|
} else
|
|
_isPOTRepository = false;
|
|
emit signalIsPOTRepository( _isPOTRepository );
|
|
}
|
|
|
|
TQString SVNHandler::fileStatus( const FileStatus status ) const
|
|
{
|
|
switch ( status ) {
|
|
case NO_REPOSITORY:
|
|
return i18n( "No SVN repository" );
|
|
break;
|
|
case NOT_IN_SVN:
|
|
return i18n( "Not in SVN" );
|
|
break;
|
|
case LOCALLY_ADDED:
|
|
return i18n( "Locally added" );
|
|
break;
|
|
case LOCALLY_REMOVED:
|
|
return i18n( "Locally removed" );
|
|
break;
|
|
case LOCALLY_MODIFIED:
|
|
return i18n( "Locally modified" );
|
|
break;
|
|
case UP_TO_DATE:
|
|
return i18n( "Up-to-date" );
|
|
break;
|
|
case CONFLICT:
|
|
return i18n( "Conflict" );
|
|
break;
|
|
case ERROR_IN_WC:
|
|
return i18n( "Error in Working Copy" );
|
|
default:
|
|
return i18n( "Unknown" );
|
|
break;
|
|
}
|
|
}
|
|
|
|
SVNHandler::FileStatus SVNHandler::fstatus( const TQString& filename ) const
|
|
{
|
|
// no valid repository
|
|
if ( !_isPORepository )
|
|
return NO_REPOSITORY;
|
|
|
|
TQString fn( filename );
|
|
fn = fn.remove( TQRegExp( "/$" ) );
|
|
|
|
TQFileInfo info( fn );
|
|
|
|
// check if '.svn/entries' exists.
|
|
TQFile entries( info.dir( true ).path( ) + "/.svn/entries" );
|
|
|
|
if ( !entries.exists() )
|
|
return NOT_IN_SVN;
|
|
|
|
TDEProcess proc;
|
|
SVNOutputCollector out( &proc );
|
|
|
|
proc << "svn" << "status" << "-v" << "--xml" << info.absFilePath();
|
|
|
|
if( !proc.start( TDEProcess::Block, TDEProcess::Stdout ) )
|
|
return ERROR_IN_WC;
|
|
|
|
TQDomDocument doc;
|
|
TQString errorMsg;
|
|
int errorLine, errorCol;
|
|
TQDomNodeList nodelist;
|
|
TQDomNode node;
|
|
TQDomElement entry, wcStatus;
|
|
|
|
// Parse the output.
|
|
if ( !doc.setContent( out.getOutput(), &errorMsg, &errorLine, &errorCol ) ) {
|
|
kdDebug(8109) << "Cannot parse \"svn status -v --xml\" output for"
|
|
<< filename << endl << "Line: " << errorLine << " Column: "
|
|
<< errorCol << " Error: " << errorMsg << endl;
|
|
goto no_status_xml;
|
|
}
|
|
|
|
// There should be only one "entry" element. If it doesn't exist, path
|
|
// isn't repo path at all.
|
|
nodelist = doc.elementsByTagName("entry");
|
|
if (nodelist.count() < 1)
|
|
return NOT_IN_SVN;
|
|
|
|
entry = nodelist.item(0).toElement();
|
|
|
|
// Shouldn't fail, but just in case there is some weird error.
|
|
if ( entry.attributeNode("path").value() != info.absFilePath() )
|
|
return ERROR_IN_WC;
|
|
|
|
for ( node = entry.firstChild(); !node.isNull(); node = node.nextSibling() ) {
|
|
if ( !node.isElement() )
|
|
continue;
|
|
if (node.toElement().tagName() == "wc-status")
|
|
break;
|
|
}
|
|
|
|
if ( node.isNull() )
|
|
return ERROR_IN_WC;
|
|
|
|
wcStatus = node.toElement();
|
|
|
|
if ( wcStatus.attributeNode("item").value() == "normal" )
|
|
return UP_TO_DATE;
|
|
if ( wcStatus.attributeNode("item").value() == "modified" )
|
|
return LOCALLY_MODIFIED;
|
|
if ( wcStatus.attributeNode("item").value() == "conflicted" )
|
|
return CONFLICT;
|
|
if ( wcStatus.attributeNode("item").value() == "unversioned" )
|
|
return NOT_IN_SVN;
|
|
// TODO Ignored entry should have separate return value probably.
|
|
if ( wcStatus.attributeNode("item").value() == "ignored" )
|
|
return NOT_IN_SVN;
|
|
if ( wcStatus.attributeNode("item").value() == "added" )
|
|
return LOCALLY_ADDED;
|
|
if ( wcStatus.attributeNode("item").value() == "deleted" )
|
|
return LOCALLY_REMOVED;
|
|
// TODO What to do with "missing", "incomplete", "replaced", "merged",
|
|
// "obstructed", "external"? Can these appear at all in our case?
|
|
|
|
return ERROR_IN_WC;
|
|
|
|
no_status_xml:
|
|
if ( !entries.open( IO_ReadOnly ) )
|
|
return ERROR_IN_WC; // we already know that it is a repository
|
|
|
|
// Parse the entries file
|
|
if ( !doc.setContent( &entries, &errorMsg, &errorLine, &errorCol ) ) {
|
|
kdDebug() << "Cannot parse .svn/entries file for " << filename << endl
|
|
<< "Line: " << errorLine << " Column: " << errorCol << " Error: " << errorMsg << endl;
|
|
return ERROR_IN_WC;
|
|
}
|
|
entries.close();
|
|
|
|
TQDomElement element;
|
|
// File name that we are searching
|
|
const TQString findName = info.fileName();
|
|
// The entries are <entry> elements, so we have to check them
|
|
TQDomNode child = doc.documentElement().firstChild();
|
|
for ( ; !child.isNull() ; child = child.nextSibling() )
|
|
{
|
|
if ( !child.isElement() )
|
|
continue;
|
|
element = child.toElement();
|
|
if ( element.tagName() != "entry" ) {
|
|
// We have another kind of element, so skip it
|
|
// Should not happend with svn 1.1.x
|
|
continue;
|
|
}
|
|
const TQString name = element.attribute("name");
|
|
if ( name == findName )
|
|
break;
|
|
}
|
|
|
|
if ( child.isNull() ) {
|
|
// We have not found an entry for the file
|
|
return NOT_IN_SVN;
|
|
}
|
|
|
|
// ### TODO: should we check the attribute kind to be file and not dir?
|
|
|
|
// ### TODO: what do copy and move add here?
|
|
const TQString onSchedule = element.attribute( "schedule" );
|
|
if ( onSchedule == "delete" )
|
|
return LOCALLY_REMOVED;
|
|
else if ( onSchedule == "added" )
|
|
return LOCALLY_ADDED;
|
|
|
|
if ( element.hasAttribute( "conflict-new" ) || element.hasAttribute( "conflict-old" ) || element.hasAttribute( "conflict-wrk" ) ) {
|
|
return CONFLICT;
|
|
}
|
|
|
|
// Note: we do not check the property time stamp
|
|
const TQString textTimeStamp( element.attribute( "text-time" ) );
|
|
|
|
// calculate the UTC time from the file's last modified date
|
|
struct stat st;
|
|
lstat( TQFile::encodeName(fn), &st );
|
|
struct tm * tm_p = gmtime( &st.st_mtime );
|
|
const int year = tm_p->tm_year + 1900;
|
|
const int month = tm_p->tm_mon + 1;
|
|
TQString fileTime;
|
|
fileTime.sprintf( "%04i-%02i-%02iT%02i:%02i:%02i.000000Z",
|
|
year, month, tm_p->tm_mday, tm_p->tm_hour, tm_p->tm_min, tm_p->tm_sec );
|
|
//kdDebug() << "File: " << filename << " SVN time: " << textTimeStamp << " File time: " << fileTime << endl;
|
|
if ( fileTime > textTimeStamp ) // ISO 8601 dates/times can be compared as strings if they have the exact same format.
|
|
return LOCALLY_MODIFIED;
|
|
|
|
return UP_TO_DATE;
|
|
|
|
}
|
|
|
|
TQString SVNHandler::svnStatus( const TQString& filename ) const
|
|
{
|
|
return map[filename];
|
|
}
|
|
|
|
void SVNHandler::execSVNCommand( TQWidget* parent, SVN::Command cmd, const TQString& filename, bool templates, TDESharedConfig* config)
|
|
{
|
|
// Unlike cvs, svn works also from outside the repository(as long as the path is in a repository of course!)
|
|
// ### FIXME: wrong, svn commit cannot work if the current directory is not a SVN one
|
|
execSVNCommand( parent, cmd, TQStringList( filename ), templates, config );
|
|
}
|
|
|
|
void SVNHandler::execSVNCommand( TQWidget* parent, SVN::Command cmd, const TQStringList& files, bool templates, TDESharedConfig* config )
|
|
{
|
|
if ( !_isPORepository ) {
|
|
// This message box should never be visible but who knows... ;-)
|
|
KMessageBox::sorry( parent, i18n( "This is not a valid SVN repository. "
|
|
"The SVN commands cannot be executed." ) );
|
|
return;
|
|
}
|
|
|
|
// ### TODO: instead of making a TQString, use TDEProcess directly, so that it cares about quoting.
|
|
// ### TODO: use TDEProcess::setWorkingDirectory instead of using "cd" (therefore allowing to use TDEProcess without a shell.)
|
|
TQString command("cd " + (templates ? _potBaseDir : _poBaseDir) + " && svn ");
|
|
switch ( cmd ) {
|
|
case SVN::Update:
|
|
command += "update --non-interactive";
|
|
break;
|
|
case SVN::Commit:
|
|
// The svn client allows to choose the encoding, so we select UTF-8
|
|
command += "commit -F @LOG@FILE@ --encoding UTF-8 --non-interactive";
|
|
checkToAdd( files );
|
|
break;
|
|
case SVN::StatusRemote:
|
|
command += "status -u --non-interactive";
|
|
break;
|
|
case SVN::StatusLocal:
|
|
command += "status --non-interactive";
|
|
break;
|
|
case SVN::Diff:
|
|
command += "diff --non-interactive";
|
|
break;
|
|
case SVN::Info:
|
|
command += "info"; // Does not allow --non-interactive (at least svn 1.1.4).
|
|
}
|
|
|
|
TQRegExp rx;
|
|
if (templates)
|
|
rx.setPattern(_potBaseDir + "/?");
|
|
else
|
|
rx.setPattern(_poBaseDir + "/?");
|
|
|
|
TQStringList::ConstIterator it;
|
|
for ( it = files.begin( ); it != files.end( ); ++it ) {
|
|
TQString temp = *it;
|
|
temp.remove(rx);
|
|
command += " \'" + temp + "\'";
|
|
}
|
|
|
|
showDialog( parent, cmd, files, command, config );
|
|
}
|
|
|
|
void SVNHandler::setAutoUpdateTemplates( bool update )
|
|
{
|
|
_autoUpdateTemplates = update;
|
|
}
|
|
|
|
void SVNHandler::showDialog( TQWidget* parent, SVN::Command cmd, const TQStringList& files, const TQString& commandLine, TDESharedConfig* config )
|
|
{
|
|
SVNDialog * dia = new SVNDialog( cmd, parent, config );
|
|
dia->setFiles( files );
|
|
dia->setCommandLine( commandLine );
|
|
if ( cmd == SVN::Commit ) {
|
|
dia->setAddCommand( _addCommand );
|
|
}
|
|
|
|
if ( dia->exec( ) == KDialog::Accepted ) {
|
|
if ( cmd == SVN::StatusLocal || cmd == SVN::StatusRemote )
|
|
processStatusOutput( dia->statusOutput( ) );
|
|
if ( cmd == SVN::Diff )
|
|
processDiff( dia->statusOutput( ) );
|
|
}
|
|
|
|
delete dia;
|
|
|
|
// file status display update necessary in Catalog Manager
|
|
if ( cmd == SVN::Commit )
|
|
emit signalFilesCommitted( files );
|
|
}
|
|
|
|
bool SVNHandler::isInSvn( const TQString& path )
|
|
{
|
|
if ( path.isEmpty() )
|
|
return false;
|
|
|
|
/*
|
|
* We need to check if a file is in a SVN repository.
|
|
*
|
|
* But as we want to do it quickly (because this function will be called for a few files in a row)
|
|
* we should avoid to parse the .svn/entries files
|
|
*
|
|
* Therefore we only check for the SVN auxilary files that are typical for a file controlled by SVN:
|
|
* - for a directory: checks if the directory has a .svn/entries file
|
|
* - for a file: check if there is a corresponding file in .svn/text-base/
|
|
*/
|
|
|
|
const TQFileInfo info( path );
|
|
if ( info.isDir() ) {
|
|
// It is a directory, so find a .svn/entries file
|
|
TQDir dir( path );
|
|
return dir.exists( ".svn/entries", true );
|
|
}
|
|
else {
|
|
// It is a file, so find the corresponding file in .svn/text-base
|
|
TQDir dir( info.dirPath() );
|
|
if ( ! dir.cd( ".svn/text-base" ) ) {
|
|
// There is not even a .svn/text-base directory, so the file is not under control
|
|
return false;
|
|
}
|
|
const TQString textBaseFilename( info.fileName() + ".svn-base" );
|
|
return dir.exists( textBaseFilename, true );
|
|
}
|
|
}
|
|
|
|
void SVNHandler::checkToAdd( const TQStringList& files )
|
|
{
|
|
if ( files.isEmpty( ) )
|
|
return;
|
|
|
|
TQStringList toBeAdded;
|
|
|
|
TQStringList::ConstIterator it;
|
|
for ( it = files.begin( ); it != files.end( ); ++it ) {
|
|
// check for every entry if it needs to be added
|
|
if ( ! isInSvn( *it ) ) {
|
|
TQFileInfo info( *it );
|
|
TQString temp; // will hold the dir path
|
|
if ( info.isDir( ) ) {
|
|
toBeAdded << *it;
|
|
temp = *it;
|
|
} else {
|
|
toBeAdded << *it;
|
|
temp = TQFileInfo( *it ).dirPath( true );
|
|
}
|
|
|
|
// ### TODO: does SVN really needs this or does it do it automatically?
|
|
// check recursivlely if parent dirs have to be added as well
|
|
while ( ! isInSvn( temp ) && toBeAdded.findIndex( temp ) == -1 ) {
|
|
toBeAdded << temp;
|
|
temp = TQFileInfo( temp ).dirPath( true );
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// remove an old command
|
|
_addCommand = TQString();
|
|
|
|
// ### TODO: does SVN really need this?
|
|
// make sure the directories are added before the files
|
|
toBeAdded.sort( );
|
|
|
|
// ### TODO: try to make this better
|
|
// create a command line for adding the files and dirs
|
|
for ( it = toBeAdded.begin( ); it != toBeAdded.end( ); ++it ) {
|
|
TQFileInfo info( *it );
|
|
_addCommand += "cd " + info.dirPath( true ) + " && svn add " + info.fileName( ) + "; ";
|
|
}
|
|
}
|
|
|
|
// ### TODO: convert to SVN
|
|
void SVNHandler::processStatusOutput( const TQString& status )
|
|
{
|
|
if ( !_isPORepository )
|
|
return;
|
|
|
|
#if 0
|
|
// at first we need to extract the name of the base directory on the server
|
|
TQFile f( _poBaseDir + "/SVN/Root" ); // ### FIXME
|
|
if ( !f.open( IO_ReadOnly ) )
|
|
return;
|
|
|
|
TQTextStream stream( &f );
|
|
// extract the string after the last colon in the first line
|
|
TQString basedir = stream.readLine( ).section( ':', -1 );
|
|
|
|
f.close( );
|
|
|
|
// divide the complete status output in little chunks for every file
|
|
TQStringList entries = TQStringList::split( TQRegExp( "={67,67}" ), status );
|
|
TQStringList::Iterator it;
|
|
for ( it = entries.begin( ); it != entries.end( ); ++it ) {
|
|
TQString entr = *it;
|
|
// translate the filename from repository to local
|
|
TQRegExp rx( basedir + ".*,v" );
|
|
int pos = entr.find( rx );
|
|
TQString file = _poBaseDir + entr.mid( pos + basedir.length( ),
|
|
rx.matchedLength( ) - basedir.length( ) - 2 );
|
|
|
|
entr = "<qt>" + entr + "</qt>";
|
|
|
|
// TODO: do some markup
|
|
|
|
map.replace( file, entr );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void SVNHandler::processDiff( TQString output )
|
|
{
|
|
output.remove( TQRegExp( "\\[ .* \\]$" ));
|
|
output.remove( TQRegExp( "^" + i18n("[ Starting command ]" ).replace("[","\\[").replace("]","\\]")));
|
|
|
|
KTempFile tmpFile;
|
|
*(tmpFile.textStream()) << output;
|
|
tmpFile.close();
|
|
|
|
TQString error;
|
|
if ( TDEApplication::startServiceByName( "Kompare", tmpFile.name(), &error ) )
|
|
KMessageBox::error( 0, error );
|
|
}
|
|
|
|
bool SVNHandler::isConsideredModified( const FileStatus status ) const
|
|
{
|
|
/*
|
|
* A file is modified if it is either:
|
|
* - locally modified for SVN
|
|
* - directory under SVN control but not the file
|
|
*/
|
|
|
|
// ### TODO: what about moved and copied?
|
|
return status == LOCALLY_MODIFIED || status == NOT_IN_SVN;
|
|
}
|
|
|
|
SVNOutputCollector::SVNOutputCollector( TDEProcess* p )
|
|
: m_process(0)
|
|
{
|
|
setProcess( p );
|
|
}
|
|
|
|
void SVNOutputCollector::setProcess( TDEProcess* p )
|
|
{
|
|
if( m_process )
|
|
m_process->disconnect( this );
|
|
|
|
m_process = p;
|
|
if( p ) {
|
|
connect( p, TQ_SIGNAL(receivedStdout(TDEProcess*, char*, int)),
|
|
this, TQ_SLOT(slotGatherStdout(TDEProcess*, char*, int)) );
|
|
connect( p, TQ_SIGNAL(receivedStderr(TDEProcess*, char*, int)),
|
|
this, TQ_SLOT(slotGatherStderr(TDEProcess*, char*, int)) );
|
|
}
|
|
|
|
m_gatheredOutput.truncate( 0 );
|
|
m_stderrOutput.truncate( 0 );
|
|
m_stdoutOutput.truncate( 0 );
|
|
}
|
|
|
|
void SVNOutputCollector::slotGatherStderr( TDEProcess*, char* data, int len )
|
|
{
|
|
m_gatheredOutput.append( TQString::fromLocal8Bit( data, len ) );
|
|
m_stderrOutput.append( TQString::fromLocal8Bit( data, len ) );
|
|
}
|
|
|
|
void SVNOutputCollector::slotGatherStdout( TDEProcess*, char* data, int len )
|
|
{
|
|
m_gatheredOutput.append( TQString::fromLocal8Bit( data, len ) );
|
|
m_stdoutOutput.append( TQString::fromLocal8Bit( data, len ) );
|
|
}
|
|
|
|
#include "svnhandler.moc"
|