Fix #120, fix #125, fix #134 - Add .griveignore

pull/59/merge
Vitaliy Filippov 2017-03-19 19:54:03 +03:00
parent a0aff5b146
commit 63bb138b2d
6 changed files with 159 additions and 34 deletions

View File

@ -85,6 +85,7 @@ Grive uses cmake to build. Basic install sequence is
### Grive2 v0.5.1-dev ### Grive2 v0.5.1-dev
- support for .griveignore
- no-remote-new and upload-only modes - no-remote-new and upload-only modes
- ignore regexp does not persist anymore (note that Grive will still track it to not - ignore regexp does not persist anymore (note that Grive will still track it to not
accidentally delete remote files when changing ignore regexp) accidentally delete remote files when changing ignore regexp)
@ -137,3 +138,21 @@ New features:
- #87: support for revisions - #87: support for revisions
- #86: ~~partial sync (contributed by justin at tierramedia.com)~~ that's not partial sync, - #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 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

View File

@ -46,10 +46,6 @@ to download only files that are changed in Google Drive and already exist locall
\fB\-h\fR, \fB\-\-help\fR \fB\-h\fR, \fB\-\-help\fR
Produces help message Produces help message
.TP .TP
\fB\-\-ignore\fR <perl_regexp>
Ignore files with relative paths matching this Perl Regular Expression.
Value is remembered for next runs.
.TP
\fB\-l\fR <filename>, \fB\-\-log\fR <filename> \fB\-l\fR <filename>, \fB\-\-log\fR <filename>
Write log output to Write log output to
.I <filename> .I <filename>
@ -81,6 +77,37 @@ Print ASCII progress bar for each downloaded/uploaded file.
\fB\-V\fR, \fB\-\-verbose\fR \fB\-V\fR, \fB\-\-verbose\fR
Verbose mode. Enables more messages than usual. 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 .SH AUTHORS
.PP .PP
Current maintainer is Vitaliy Filippov. Current maintainer is Vitaliy Filippov.

View File

@ -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" ) ( "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, " ( "dry-run", "Only detect which files need to be uploaded/downloaded, "
"without actually performing them." ) "without actually performing them." )
( "ignore", po::value<std::string>(), "Perl RegExp to ignore files (matched against relative paths)." )
( "upload-speed,U", po::value<unsigned>(), "Limit upload speed in kbytes per second" ) ( "upload-speed,U", po::value<unsigned>(), "Limit upload speed in kbytes per second" )
( "download-speed,D", po::value<unsigned>(), "Limit download speed in kbytes per second" ) ( "download-speed,D", po::value<unsigned>(), "Limit download speed in kbytes per second" )
( "progress-bar,P", "Enable progress bar for upload/download of files") ( "progress-bar,P", "Enable progress bar for upload/download of files")
; ;
po::variables_map vm; po::variables_map vm;
po::store(po::parse_command_line( argc, argv, desc), vm ); try
po::notify(vm); {
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 // simple commands that doesn't require log or config
if ( vm.count("help") ) if ( vm.count("help") )
@ -148,9 +155,9 @@ int Main( int argc, char **argv )
} }
// initialize logging // initialize logging
InitLog(vm) ; InitLog( vm ) ;
Config config(vm) ; Config config( vm ) ;
Log( "config file name %1%", config.Filename(), log::verbose ); Log( "config file name %1%", config.Filename(), log::verbose );

View File

@ -41,15 +41,10 @@
namespace gr { namespace gr {
namespace
{
const std::string state_file = ".grive_state" ;
}
Drive::Drive( Syncer *syncer, const Val& options ) : Drive::Drive( Syncer *syncer, const Val& options ) :
m_syncer ( syncer ), m_syncer ( syncer ),
m_root ( options["path"].Str() ), m_root ( options["path"].Str() ),
m_state ( m_root / state_file, options ), m_state ( m_root, options ),
m_options ( options ) m_options ( options )
{ {
assert( m_syncer ) ; assert( m_syncer ) ;
@ -72,7 +67,7 @@ void Drive::FromChange( const Entry& entry )
void Drive::SaveState() void Drive::SaveState()
{ {
m_state.Write( m_root / state_file ) ; m_state.Write() ;
} }
void Drive::DetectChanges() void Drive::DetectChanges()

View File

@ -32,17 +32,28 @@
namespace gr { 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_res ( options["path"].Str() ),
m_cstamp ( -1 ) m_cstamp ( -1 )
{ {
Read( filename ) ; Read() ;
// the "-f" option will make grive always think remote is newer // the "-f" option will make grive always think remote is newer
m_force = options.Has( "force" ) ? options["force"].Bool() : false ; m_force = options.Has( "force" ) ? options["force"].Bool() : false ;
std::string m_orig_ign = m_ign; std::string m_orig_ign = m_ign;
m_ign = "";
if ( options.Has( "ignore" ) && options["ignore"].Str() != m_ign ) if ( options.Has( "ignore" ) && options["ignore"].Str() != m_ign )
m_ign = options["ignore"].Str(); m_ign = options["ignore"].Str();
else if ( options.Has( "dir" ) ) else if ( options.Has( "dir" ) )
@ -52,8 +63,7 @@ State::State( const fs::path& filename, const Val& options ) :
if ( !m_dir.empty() ) if ( !m_dir.empty() )
{ {
// "-s" is internally converted to an ignore regexp // "-s" is internally converted to an ignore regexp
const boost::regex esc( "[.^$|()\\[\\]{}*+?\\\\]" ); m_dir = regex_escape( m_dir );
m_dir = regex_replace( m_dir, esc, "\\\\&", boost::format_sed );
size_t pos = 0; size_t pos = 0;
while ( ( pos = m_dir.find( '/', pos ) ) != std::string::npos ) 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_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() State::~State()
@ -277,27 +287,92 @@ State::iterator State::end()
return m_res.end() ; return m_res.end() ;
} }
void State::Read( const fs::path& filename ) void State::Read()
{ {
try try
{ {
File file( filename ) ; File st_file( m_root / state_file ) ;
m_st = ParseJson( st_file );
m_st = ParseJson( file );
m_ign = m_st.Has( "ignore_regexp" ) ? m_st["ignore_regexp"].Str() : std::string();
m_cstamp = m_st["change_stamp"].Int() ; m_cstamp = m_st["change_stamp"].Int() ;
} }
catch ( Exception& ) 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( "change_stamp", Val( m_cstamp ) ) ;
m_st.Set( "ignore_regexp", Val( m_ign ) ) ; m_st.Set( "ignore_regexp", Val( m_ign ) ) ;
fs::path filename = m_root / state_file ;
std::ofstream fs( filename.string().c_str() ) ; std::ofstream fs( filename.string().c_str() ) ;
fs << m_st ; fs << m_st ;
} }

View File

@ -42,15 +42,15 @@ public :
typedef ResourceTree::iterator iterator ; typedef ResourceTree::iterator iterator ;
public : public :
explicit State( const fs::path& filename, const Val& options ) ; explicit State( const fs::path& root, const Val& options ) ;
~State() ; ~State() ;
void FromLocal( const fs::path& p ) ; void FromLocal( const fs::path& p ) ;
void FromRemote( const Entry& e ) ; void FromRemote( const Entry& e ) ;
void ResolveEntry() ; void ResolveEntry() ;
void Read( const fs::path& filename ) ; void Read() ;
void Write( const fs::path& filename ) ; void Write() ;
Resource* FindByHref( const std::string& href ) ; Resource* FindByHref( const std::string& href ) ;
Resource* FindByID( const std::string& id ) ; Resource* FindByID( const std::string& id ) ;
@ -64,6 +64,7 @@ public :
void ChangeStamp( long cstamp ) ; void ChangeStamp( long cstamp ) ;
private : private :
bool ParseIgnoreFile( const char* buffer, int size ) ;
void FromLocal( const fs::path& p, Resource *folder, Val& tree ) ; void FromLocal( const fs::path& p, Resource *folder, Val& tree ) ;
void FromChange( const Entry& e ) ; void FromChange( const Entry& e ) ;
bool Update( const Entry& e ) ; bool Update( const Entry& e ) ;
@ -72,6 +73,7 @@ private :
bool IsIgnore( const std::string& filename ) ; bool IsIgnore( const std::string& filename ) ;
private : private :
fs::path m_root ;
ResourceTree m_res ; ResourceTree m_res ;
int m_cstamp ; int m_cstamp ;
std::string m_ign ; std::string m_ign ;