diff --git a/README.md b/README.md index 2e2ee46..51941c5 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Grive uses cmake to build. Basic install sequence is ### Grive2 v0.5.1-dev +- support for .griveignore - no-remote-new and upload-only modes - ignore regexp does not persist anymore (note that Grive will still track it to not accidentally delete remote files when changing ignore regexp) @@ -137,3 +138,21 @@ New features: - #87: support for revisions - #86: ~~partial sync (contributed by justin at tierramedia.com)~~ that's not partial sync, that's only support for specifying local path on command line + +## .griveignore + +Rules are similar to Git's .gitignore, but may differ slightly due to the different +implementation. + +- lines that start with # are comments +- leading and trailing spaces ignored unless escaped with \ +- non-empty lines without ! in front are treated as "exclude" patterns +- non-empty lines with ! in front are treated as "include" patterns + and have a priority over all "exclude" ones +- patterns are matched against the filenames relative to the grive root +- a/**/b matches any number of subpaths between a and b, including 0 +- **/a matches `a` inside any directory +- b/** matches everything inside `b`, but not b itself +- \* matches any number of any characters except / +- ? matches any character except / +- .griveignore itself isn't ignored by default, but you can include it in itself to ignore diff --git a/grive/doc/grive.1 b/grive/doc/grive.1 index cf0bc26..a00aef6 100644 --- a/grive/doc/grive.1 +++ b/grive/doc/grive.1 @@ -46,10 +46,6 @@ to download only files that are changed in Google Drive and already exist locall \fB\-h\fR, \fB\-\-help\fR Produces help message .TP -\fB\-\-ignore\fR -Ignore files with relative paths matching this Perl Regular Expression. -Value is remembered for next runs. -.TP \fB\-l\fR , \fB\-\-log\fR Write log output to .I @@ -81,6 +77,37 @@ Print ASCII progress bar for each downloaded/uploaded file. \fB\-V\fR, \fB\-\-verbose\fR Verbose mode. Enables more messages than usual. +.SH .griveignore +.PP +You may create .griveignore in your Grive root and use it to setup +exclusion/inclusion rules. +.PP +Rules are similar to Git's .gitignore, but may differ slightly due to the different +implementation. +.IP \[bu] +lines that start with # are comments +.IP \[bu] +leading and trailing spaces ignored unless escaped with \\ +.IP \[bu] +non-empty lines without ! in front are treated as "exclude" patterns +.IP \[bu] +non-empty lines with ! in front are treated as "include" patterns +and have a priority over all "exclude" ones +.IP \[bu] +patterns are matched against the filenames relative to the grive root +.IP \[bu] +a/**/b matches any number of subpaths between a and b, including 0 +.IP \[bu] +**/a matches `a` inside any directory +.IP \[bu] +b/** matches everything inside `b`, but not b itself +.IP \[bu] +* matches any number of any characters except / +.IP \[bu] +? matches any character except / +.IP \[bu] +\[char46]griveignore itself isn't ignored by default, but you can include it in itself to ignore + .SH AUTHORS .PP Current maintainer is Vitaliy Filippov. diff --git a/grive/src/main.cc b/grive/src/main.cc index c87a854..b5a0a00 100644 --- a/grive/src/main.cc +++ b/grive/src/main.cc @@ -124,15 +124,22 @@ int Main( int argc, char **argv ) ( "no-remote-new,n", "Download only files that are changed in Google Drive and already exist locally" ) ( "dry-run", "Only detect which files need to be uploaded/downloaded, " "without actually performing them." ) - ( "ignore", po::value(), "Perl RegExp to ignore files (matched against relative paths)." ) ( "upload-speed,U", po::value(), "Limit upload speed in kbytes per second" ) ( "download-speed,D", po::value(), "Limit download speed in kbytes per second" ) ( "progress-bar,P", "Enable progress bar for upload/download of files") ; po::variables_map vm; - po::store(po::parse_command_line( argc, argv, desc), vm ); - po::notify(vm); + try + { + po::store( po::parse_command_line( argc, argv, desc ), vm ); + } + catch( po::error &e ) + { + std::cerr << "Options are incorrect. Use -h for help\n"; + return -1; + } + po::notify( vm ); // simple commands that doesn't require log or config if ( vm.count("help") ) @@ -148,9 +155,9 @@ int Main( int argc, char **argv ) } // initialize logging - InitLog(vm) ; + InitLog( vm ) ; - Config config(vm) ; + Config config( vm ) ; Log( "config file name %1%", config.Filename(), log::verbose ); diff --git a/libgrive/src/base/Drive.cc b/libgrive/src/base/Drive.cc index fd26225..28fd29a 100644 --- a/libgrive/src/base/Drive.cc +++ b/libgrive/src/base/Drive.cc @@ -41,15 +41,10 @@ namespace gr { -namespace -{ - const std::string state_file = ".grive_state" ; -} - Drive::Drive( Syncer *syncer, const Val& options ) : m_syncer ( syncer ), m_root ( options["path"].Str() ), - m_state ( m_root / state_file, options ), + m_state ( m_root, options ), m_options ( options ) { assert( m_syncer ) ; @@ -72,7 +67,7 @@ void Drive::FromChange( const Entry& entry ) void Drive::SaveState() { - m_state.Write( m_root / state_file ) ; + m_state.Write() ; } void Drive::DetectChanges() diff --git a/libgrive/src/base/State.cc b/libgrive/src/base/State.cc index 9fddf4d..80e2a2d 100644 --- a/libgrive/src/base/State.cc +++ b/libgrive/src/base/State.cc @@ -32,17 +32,28 @@ namespace gr { -State::State( const fs::path& filename, const Val& options ) : +const std::string state_file = ".grive_state" ; +const std::string ignore_file = ".griveignore" ; +const int MAX_IGN = 65536 ; +const char* regex_escape_chars = ".^$|()[]{}*+?\\"; +const boost::regex regex_escape_re( "[.^$|()\\[\\]{}*+?\\\\]" ); + +inline std::string regex_escape( std::string s ) +{ + return regex_replace( s, regex_escape_re, "\\\\&", boost::format_sed ); +} + +State::State( const fs::path& root, const Val& options ) : + m_root ( root ), m_res ( options["path"].Str() ), m_cstamp ( -1 ) { - Read( filename ) ; - + Read() ; + // the "-f" option will make grive always think remote is newer m_force = options.Has( "force" ) ? options["force"].Bool() : false ; - + std::string m_orig_ign = m_ign; - m_ign = ""; if ( options.Has( "ignore" ) && options["ignore"].Str() != m_ign ) m_ign = options["ignore"].Str(); else if ( options.Has( "dir" ) ) @@ -52,8 +63,7 @@ State::State( const fs::path& filename, const Val& options ) : if ( !m_dir.empty() ) { // "-s" is internally converted to an ignore regexp - const boost::regex esc( "[.^$|()\\[\\]{}*+?\\\\]" ); - m_dir = regex_replace( m_dir, esc, "\\\\&", boost::format_sed ); + m_dir = regex_escape( m_dir ); size_t pos = 0; while ( ( pos = m_dir.find( '/', pos ) ) != std::string::npos ) { @@ -66,7 +76,7 @@ State::State( const fs::path& filename, const Val& options ) : } m_ign_changed = m_orig_ign != "" && m_orig_ign != m_ign; - m_ign_re = boost::regex( m_ign.empty() ? "^\\.(grive|grive_state|trash)" : ( m_ign+"|^\\.(grive|grive_state|trash)" ) ); + m_ign_re = boost::regex( m_ign.empty() ? "^\\.(grive$|grive_state$|trash)" : ( m_ign+"|^\\.(grive|grive_state|trash)" ) ); } State::~State() @@ -277,27 +287,92 @@ State::iterator State::end() return m_res.end() ; } -void State::Read( const fs::path& filename ) +void State::Read() { try { - File file( filename ) ; - - m_st = ParseJson( file ); - m_ign = m_st.Has( "ignore_regexp" ) ? m_st["ignore_regexp"].Str() : std::string(); - + File st_file( m_root / state_file ) ; + m_st = ParseJson( st_file ); m_cstamp = m_st["change_stamp"].Int() ; } catch ( Exception& ) { } + + try + { + File ign_file( m_root / ignore_file ) ; + char ign[MAX_IGN] = { 0 }; + int s = ign_file.Read( ign, MAX_IGN-1 ) ; + ParseIgnoreFile( ign, s ); + } + catch ( Exception& e ) + { + throw e; + } } -void State::Write( const fs::path& filename ) +bool State::ParseIgnoreFile( const char* buffer, int size ) +{ + const boost::regex re1( "/\\\\\\*\\\\\\*$" ); + const boost::regex re2( "^\\\\\\*\\\\\\*/" ); + const boost::regex re3( "([^\\\\](\\\\\\\\)*)/\\\\\\*\\\\\\*/" ); + const boost::regex re4( "([^\\\\](\\\\\\\\)*|^)\\\\\\*" ); + const boost::regex re5( "([^\\\\](\\\\\\\\)*|^)\\\\\\?" ); + std::string exclude_re, include_re; + int prev = 0; + for ( int i = 0; i <= size; i++ ) + { + if ( buffer[i] == '\n' || ( i == size && i > prev ) ) + { + while ( prev < i && ( buffer[prev] == ' ' || buffer[prev] == '\t' || buffer[prev] == '\r' ) ) + prev++; + if ( buffer[prev] != '#' ) + { + int j; + for ( j = i-1; j > prev; j-- ) + if ( buffer[j-1] == '\\' || ( buffer[j] != ' ' && buffer[j] != '\t' && buffer[j] != '\r' ) ) + break; + std::string str( buffer+prev, j+1-prev ); + bool inc = str[0] == '!'; + if ( inc ) + str = str.substr( 1 ); + str = regex_escape( str ); + str = regex_replace( str, re1, "/.*", boost::format_perl ); + str = regex_replace( str, re2, ".*/", boost::format_perl ); + str = regex_replace( str, re3, "$1/(.*/)*", boost::format_perl ); + str = regex_replace( str, re4, "$1[^/]*", boost::format_perl ); + std::string str1; + while (1) + { + str1 = regex_replace( str, re5, "$1[^/]", boost::format_perl ); + if ( str1.size() == str.size() ) + break; + str = str1; + } + if ( !inc ) + exclude_re = exclude_re + ( exclude_re.size() > 0 ? "|" : "" ) + str; + else + include_re = include_re + ( include_re.size() > 0 ? "|" : "" ) + str; + } + prev = i+1; + } + } + + if ( exclude_re.size() > 0 ) + { + m_ign = "^" + ( include_re.size() > 0 ? "(?!" + include_re + ")" : std::string() ) + "(" + exclude_re + ")$"; + return true; + } + return false; +} + +void State::Write() { m_st.Set( "change_stamp", Val( m_cstamp ) ) ; m_st.Set( "ignore_regexp", Val( m_ign ) ) ; + fs::path filename = m_root / state_file ; std::ofstream fs( filename.string().c_str() ) ; fs << m_st ; } diff --git a/libgrive/src/base/State.hh b/libgrive/src/base/State.hh index 68b6df8..b42a32e 100644 --- a/libgrive/src/base/State.hh +++ b/libgrive/src/base/State.hh @@ -42,15 +42,15 @@ public : typedef ResourceTree::iterator iterator ; public : - explicit State( const fs::path& filename, const Val& options ) ; + explicit State( const fs::path& root, const Val& options ) ; ~State() ; void FromLocal( const fs::path& p ) ; void FromRemote( const Entry& e ) ; void ResolveEntry() ; - void Read( const fs::path& filename ) ; - void Write( const fs::path& filename ) ; + void Read() ; + void Write() ; Resource* FindByHref( const std::string& href ) ; Resource* FindByID( const std::string& id ) ; @@ -64,6 +64,7 @@ public : void ChangeStamp( long cstamp ) ; private : + bool ParseIgnoreFile( const char* buffer, int size ) ; void FromLocal( const fs::path& p, Resource *folder, Val& tree ) ; void FromChange( const Entry& e ) ; bool Update( const Entry& e ) ; @@ -72,6 +73,7 @@ private : bool IsIgnore( const std::string& filename ) ; private : + fs::path m_root ; ResourceTree m_res ; int m_cstamp ; std::string m_ign ;