/* * Copyright 1995, 2003 Perforce Software. All rights reserved. * * This file is part of Perforce - the FAST SCM System. * * Git style ignore file for add/reconcile */ # include <stdhdrs.h> # include <strbuf.h> # include <strops.h> # include <error.h> # include <strarray.h> # include <vararray.h> # include <debug.h> # include <pathsys.h> # include <filesys.h> # include <maptable.h> # include "ignore.h" # ifdef OS_NT # define SLASH "\\" # else # define SLASH "/" # endif # define ELLIPSE "..." /* * IgnoreTable -- cached ignore rules */ class IgnoreItem { public: IgnoreItem() { ignoreList = new StrArray; } ~IgnoreItem() { delete ignoreList; } void AppendToList( StrArray *list ) { for( int i = 0; i < ignoreList->Count(); i++ ) list->Put()->Set( ignoreList->Get( i ) ); } StrBuf ignoreFile; StrArray* ignoreList; } ; class IgnoreTable : public VarArray { public: ~IgnoreTable(); IgnoreItem *GetItem( const StrRef &file ); IgnoreItem *PutItem( const StrRef &file ); } ; IgnoreTable::~IgnoreTable() { for( int i = 0; i < Count(); i++ ) delete (IgnoreItem *) Get( i ); } IgnoreItem * IgnoreTable::GetItem( const StrRef &file ) { IgnoreItem *a; for( int i = 0; i < Count(); i++ ) { a = (IgnoreItem *)Get(i); if( !a->ignoreFile.SCompare( file ) ) return a; } return 0; } IgnoreItem * IgnoreTable::PutItem( const StrRef &file ) { IgnoreItem *a = GetItem( file ); if( !a ) { a = new IgnoreItem; a->ignoreFile.Set( file ); VarArray::Put( a ); } return a; } /* * Ignore * * Loads ignore rules from ignore files and checks to see if files match * those fules. */ Ignore::Ignore() { ignoreTable = new IgnoreTable; ignoreFiles = new StrArray; ignoreList = 0; } Ignore::~Ignore() { delete ignoreTable; delete ignoreFiles; if( ignoreList ) delete ignoreList; } int Ignore::Reject( const StrPtr &path, const StrPtr &ignoreName, const char *configName, StrBuf *line ) { return Build( path, ignoreName, configName ) && RejectCheck( path, 0, line ) ? 1 : 0; } int Ignore::RejectDir( const StrPtr &path, const StrPtr &ignoreName, const char *configName, StrBuf *line ) { return Build( path, ignoreName, configName ) && RejectCheck( path, 1, line ) ? 1 : 0; } int Ignore::List( const StrPtr &path, const StrPtr &ignoreName, const char *configName, StrArray *outList ) { Build( path, ignoreName, configName ); for( int j = 0; j < ignoreList->Count(); ++j ) outList->Put()->Set( ignoreList->Get( j ) ); return outList->Count(); } int Ignore::GetIgnoreFiles( const StrPtr &ignoreName, int absolute, int relative, StrArray &ignoreFiles ) { StrRef slash( SLASH ); const StrBuf *ign = 0; int i = 0; int res = 0; BuildIgnoreFiles( ignoreName ); while( ( ign = this->ignoreFiles->Get( i++ ) ) ) { if( ign->Contains( slash ) && absolute ) { ignoreFiles.Put()->Set( ign ); res++; } else if( !ign->Contains( slash ) && relative ) { ignoreFiles.Put()->Set( ign ); res++; } } return res; } int Ignore::Build( const StrPtr &path, const StrPtr &ignoreName, const char *configName ) { // If we don't have an ignore file, we can just load up the defaults // and test against those. If we've already loaded them, lets not do // it again and use the existing list instead. if( !strcmp( ignoreName.Text(), "unset" ) ) { if( !ignoreList ) ignoreList = new StrArray; if( !ignoreList->Count() ) InsertDefaults( ignoreList, configName ); return 1; } PathSys *p = PathSys::Create(); p->Set( path ); p->ToParent(); StrBuf saveDepth; // Try real hard not to regenerate the ignorelist, this // optimization uses the current directory depth and the // last found ignorefile depth to reduce the search for // config files. if( ignoreList && dirDepth.Length() ) { if( !dirDepth.SCompare( *p ) ) { // matching depth bail early delete p; return 1; } else if( !dirDepth.SCompareN( *p ) ) { // descending directories can be shortcut. saveDepth << dirDepth; } else if( !p->SCompareN( dirDepth ) && foundDepth.Length() && !foundDepth.SCompareN( *p ) ) { // ascending directories can be shortcut. dirDepth.Set( *p ); delete p; return 1; } else if( !dirDepth.SCompareN( *p ) ) { // descending directories can be shortcut. saveDepth << dirDepth; } else if( !p->SCompareN( dirDepth ) && foundDepth.Length() && !foundDepth.SCompareN( *p ) ) { // ascending directories can be shortcut. dirDepth.Set( *p ); delete p; return 1; } } // Split the potential ignoreName list into a real list BuildIgnoreFiles( ignoreName ); StrBuf line; StrBuf closestFound; int found = 0; Error e; PathSys *q = PathSys::Create(); FileSys *f = FileSys::Create( FileSysType( FST_TEXT|FST_L_CRLF ) ); IgnoreItem *ignoreItem = 0; const StrBuf *ignoreFile; closestFound.Clear(); dirDepth.Set( *p ); // No descending optimization, remove list we will recreate it StrArray newList; InsertDefaults( &newList, configName ); for( int m = 0; m < ignoreFiles->Count(); m++ ) { ignoreFile = ignoreFiles->Get( m ); if( ignoreFile->Contains( StrRef( SLASH ) ) ) { // load the file; we have a path, so no need to wander the // tree looking for it if( !( ignoreItem = ignoreTable->GetItem( *ignoreFile ) ) ) { ignoreItem = ignoreTable->PutItem( *ignoreFile ); f->Set( *ignoreFile ); if( !ParseFile( f, "", ignoreItem->ignoreList ) ) continue; found++; } ignoreItem->AppendToList( &newList ); } else { // starting from the directory in which the argument supplied // file lives, walk up the tree collecting ignore files as // we go p->Set( path ); p->ToParent(); do { q->SetLocal( *p, *ignoreFile ); if( !( ignoreItem = ignoreTable->GetItem( *q ) ) ) { ignoreItem = ignoreTable->PutItem( *q ); f->Set( *q ); if( !ParseFile( f, p->Text(), ignoreItem->ignoreList ) ) continue; found++; if( closestFound.Length() < p->Length() ) closestFound = *p; } ignoreItem->AppendToList( &newList ); } while( p->ToParent() ); } } if( closestFound.Length() && !foundDepth.SCompareN( closestFound ) ) { found++; foundDepth = closestFound; } if( found || !ignoreList ) { if( ignoreList ) delete ignoreList; ignoreList = new StrArray; for( int i = 0; i < newList.Count(); i++ ) ignoreList->Put()->Set( newList.Get( i ) ); } delete q; delete p; delete f; if( DEBUG_LIST ) { p4debug.printf( "\n\tIgnore list:\n\n" ); for( int j = 0; j < ignoreList->Count(); ++j ) { char *p = ignoreList->Get( j )->Text(); p4debug.printf( "\t%s\n", p ); } p4debug.printf( "\n" ); } return 1; } void Ignore::InsertDefaults( StrArray *list, const char *configName ) { StrArray defaultsList; int l = 0; StrBuf configDirLine; configDirLine.Clear(); // Always add in .p4root and P4CONFIG to the top of new lists if( configName ) { StrBuf line; line << "**/" << configName; Insert( &defaultsList, line.Text(), "", ++l ); configDirLine << ".../" << configName << SLASH << "..."; } Insert( &defaultsList, "**/.p4root", "", ++l ); // Add debug line list->Put()->Set( StrRef( "#FILE - defaults" ) ); // Add the generated lines to the ignore list (in reverse) StrBuf line; for( int i = defaultsList.Count(); i > 0; --i ) { if( configName && *defaultsList.Get( i - 1 ) == configDirLine ) continue; line.Set( defaultsList.Get( i - 1 ) ); #ifdef OS_NT // On NT slash is \ but the matcher's * wildcard only uses /, // so we need to convert our slashes StrOps::Sub( line, '\\', '/' ); #endif list->Put()->Set( line ); } } void Ignore::Insert( StrArray *subList, const char *ignore, const char *cwd, int lineno ) { StrBuf buf; StrBuf buf2; StrBuf raw = ignore; const char *lastCwdChar = cwd + strlen( cwd ) - 1; const char *lastIgnChar = ignore + strlen( ignore ) - 1; char *terminating = (char *)SLASH; int reverse = ( *ignore == '!' ); int isWild = strchr( ignore, '*' ) != 0; // One slash at the end is a sign that we're just matching directories int isDir = ( *lastIgnChar == *terminating ); if( strstr( ignore, "*****" ) || strstr( ignore, "..." ) ) buf << "### SENSELESS JUXTAPOSITION "; if( reverse ) { buf << "!"; ignore++; } // If the ignore line starts with a slash, it's relative to this ingore file int isRel = ( *ignore == *terminating ); if( isRel ) ignore++; // Add the base path - in needs a trailing / unless it's not set (global) buf << cwd; if( strlen( cwd ) && *lastCwdChar != *terminating ) buf << SLASH; // buf contains the relative path // buf2 contains the non-relative path // If the path is relative (starts with /) then we don't want to // add .../ to the start. // Otherwise we will need both (to include files here and in // directories below us) buf2 << buf << ELLIPSE; buf << ignore; if( !isRel && *ignore == '*' ) { // The path isn't relative, so we'll use buf2. // If it starts with a * (or **) lets not add the slash. // E.g. *.foo/bar -> ....foo/bar (not .../*.foo/bar) while( *ignore == '*' ) ignore++; buf2 << ignore; } else buf2 << SLASH << ignore; if( isDir ) { // The path ends with a shash, so add the ... buf << ELLIPSE; buf2 << ELLIPSE; } // It's possible that we've expanded out buf2 so that we no longer // need buf (E.g. *.foo/bar). If the path was wild, but isn't // anymore, then lets not add it. if( isRel || !isWild || ( isWild && strchr( ignore, '*' ) != 0 ) ) StrOps::Replace( *subList->Put(), buf, StrRef( "**" ), StrRef( ELLIPSE )); if( !isRel ) StrOps::Replace( *subList->Put(), buf2, StrRef( "**" ), StrRef( ELLIPSE )); // If the path was not explicitly a directory, it might be one. // Unless of course it edded with a or ** if( !isDir && !buf.EndsWith( "**", 2 ) ) { buf << SLASH << ELLIPSE; buf2 << SLASH << ELLIPSE; if( isRel || !isWild || ( isWild && strchr( ignore, '*' ) != 0 ) ) StrOps::Replace( *subList->Put(), buf, StrRef( "**" ), StrRef( ELLIPSE )); if( !isRel ) StrOps::Replace( *subList->Put(), buf2, StrRef( "**" ), StrRef( ELLIPSE )); } // Add the debug line buf.Clear(); buf << "#LINE " << lineno << ":" << raw; subList->Put()->Set( buf ); } int Ignore::ParseFile( FileSys *f, const char *cwd, StrArray *list ) { Error e; StrBuf line; StrBuf dline; StrArray tmpList; f->Open( FOM_READ, &e ); if( e.Test() ) return 0; for( int l = 1; f->ReadLine( &line, &e ); l++ ) { line.TrimBlanks(); if( !line.Length() || line.Text()[0] == '#' ) continue; if( line.Text()[0] == '\\' && line.Text()[1] == '#' ) { StrBuf tmp( line.Text() + 1 ); line = tmp; } #ifdef OS_NT // On NT slash is \ so we need to normalise our slashes to // avoid cross platform issues. StrOps::Sub( line, '/', '\\' ); #endif Insert( &tmpList, line.Text(), cwd, l ); } f->Close( &e ); line.Clear(); line << "#FILE " << f->Name(); list->Put()->Set( line ); // Add the read lines to the target list (in reverse) for( int i = tmpList.Count(); i > 0; --i ) { line.Set( tmpList.Get( i - 1 ) ); #ifdef OS_NT // On NT slash is \ but the matcher's * wildcard only uses /, // so we need to convert our slashes StrOps::Sub( line, '\\', '/' ); #endif list->Put()->Set( line ); } return 1; } int Ignore::RejectCheck( const StrPtr &path, int isDir, StrBuf *line ) { char *ignoreFile = 0; char *ignoreLine = 0; // Fix the path separators StrBuf cpath( path ); StrOps::Sub( cpath, '\\', '/' ); // Dirs must have trailing / for matching /... if( isDir && !cpath.EndsWith( "/", 1 ) ) cpath << "/"; // Dirs have /... tails when checking in reverse StrBuf dpath( cpath ); dpath << "..."; for( int i = 0; i < ignoreList->Count(); ++i ) { char *p = ignoreList->Get( i )->Text(); if( !strncmp( p, "#FILE ", 6 ) ) { ignoreFile = p+6; continue; } if( !strncmp( p, "#LINE ", 6 ) ) { ignoreLine = p+6; continue; } int doAdd = ( *p == '!' ); if( doAdd ) ++p; // If we're checking against a directory and this is a reverse // include, it might allow files below this directoryt, even if // this directory is ignored. To deal with this, we need to look // both ways check match in either direction) if( MapTable::Match( StrRef( p ), cpath ) || ( isDir && doAdd && MapTable::Match( dpath, StrRef( p ) ) ) ) { if( DEBUG_MATCH ) p4debug.printf( "\n\t%s[%s]\n\tmatch[%s%s]%s\n\tignore[%s]\n\n", isDir ? "dir" : "file", path.Text(), doAdd ? "+" : "-", p, doAdd ? "KEEP" : "REJECT", ignoreFile ); // If an ignoreLine pointer was passed, populate it with the // ignoreFile, line number and rule that we matched. if( line && ignoreFile && ignoreLine ) { line->Set( ignoreFile ); line->UAppend( ":" ); line->UAppend( ignoreLine ); } return doAdd ? 0 : 1; } } return 0; } void Ignore::BuildIgnoreFiles( const StrPtr &ignoreNames ) { if( ignoreStr == ignoreNames ) return; if( ignoreFiles ) delete ignoreFiles; ignoreFiles = new StrArray; // We might have more than one ignore file name set: split them up if( strchr( ignoreNames.Text(), ';' ) || ( SLASH[0] == '/' && strchr( ignoreNames.Text(), ':' ) ) ) { StrBuf iname = ignoreNames; // Do some work to make sure we have the right path seaprators // and normalise the split characters. #ifdef OS_NT StrOps::Sub( iname, '/', '\\' ); #else StrOps::Sub( iname, '\\', '/' ); StrOps::Sub( iname, ':', ';' ); #endif char *c; char *n = iname.Text(); while( c = strchr( n, ';' ) ) { if( c > n ) ignoreFiles->Put()->Set( StrRef( n, c - n ) ); n = c + 1; } if( strlen( n ) ) ignoreFiles->Put()->Set( StrRef( n, strlen( n ) ) ); } else ignoreFiles->Put()->Set( ignoreNames ); ignoreStr = ignoreNames; }