1 module todod.storage; 2 3 import core.time : msecs; 4 import std.algorithm; 5 import std.concurrency : spawn, receive, receiveTimeout, send, thisTid, Tid; 6 import std.conv; 7 import std.exception; 8 import std.file; 9 import std.regex; 10 import std.stdio; 11 import std.string; 12 import std.uuid; 13 14 import deimos.git2.all; 15 //import dinotify : iNotify, INotify, IN_MODIFY; 16 import dinotify; 17 import core.sys.linux.sys.inotify; 18 19 import todod.commandline; 20 import todod.state; 21 22 void fileActor( Tid actor, string path, string file ) 23 { 24 auto monitor = iNotify(); 25 //monitor.add( _path[0..$-1].ptr, IN_CREATE | IN_DELETE ); 26 auto watch = monitor.add( (path ~ file).toStringz, IN_CLOSE ); 27 28 auto events = monitor.read(); 29 debug writeln( "Sending events length: ", file ); 30 actor.send( "fileEvent", file ); 31 monitor.remove( watch ); 32 } 33 34 void fileMonitor( Tid actor, string file ) 35 { 36 auto monitor = iNotify(); 37 //monitor.add( _path[0..$-1].ptr, IN_CREATE | IN_DELETE ); 38 monitor.add( file.toStringz, IN_CLOSE ); 39 40 while (true) 41 { 42 auto events = monitor.read(); 43 debug writeln( "Sending events length" ); 44 actor.send( events.length ); 45 } 46 } 47 48 /// Interact with a file 49 class MonitoredFile 50 { 51 this( string path, string name ) 52 { 53 _path = path; 54 if (_path[$-1] != '/') 55 _path ~= "/"; 56 _name = name; 57 58 // Make sure it exists 59 mkdirRecurse( _path ); 60 if( !exists( _path ~ _name ) ) 61 { 62 File file = File( _path ~ _name, "w" ); 63 file.writeln( "" ); 64 file.close; 65 } 66 67 // TODO this fails if other process is already watching the file 68 monitorActor = spawn( &fileMonitor, thisTid, _path ~ _name ); 69 } 70 71 bool changed( int timeOut = 0 ) 72 { 73 if( receiveTimeout( timeOut.msecs, 74 (size_t v) { debug writeln("File was changed"); } ) ) 75 return true; 76 return false; 77 } 78 79 void write( string contents ) 80 { 81 File file = File( _path ~ _name, "w" ); 82 file.writeln( contents ); 83 file.close; 84 // Waiting for monitor to find the changed file 85 receive( (size_t v) { debug writeln("File was changed"); } ); 86 } 87 88 private: 89 string _path; 90 string _name; 91 Tid monitorActor; 92 } 93 94 unittest 95 { 96 auto mFile = new MonitoredFile( "path/", "file" ); 97 assert( mFile._path == "path/" ); 98 assert( mFile._name == "file" ); 99 // Check path works without trailing / 100 mFile = new MonitoredFile( "path", "file2" ); 101 assert( mFile._path == "path/" ); 102 assert( mFile._name == "file2" ); 103 104 // Check changed is false if file did not exist 105 assert( !mFile.changed ); 106 107 // Check changed is false when writing through MonitoredFile.write 108 mFile.write( "bla" ); 109 assert( !mFile.changed ); 110 111 // Check changed is true if writing in between 112 File file = File( "path/file2", "w" ); 113 file.writeln( "editting during test" ); 114 file.close; 115 assert( mFile.changed( 1000 ) ); 116 117 // Make sure to clean up 118 rmdirRecurse( "path" ); 119 } 120 121 /// Write string contents to file at the given path 122 void writeToFile( string path, string name, string contents ) { 123 auto fileName = path ~ "/" ~ name; 124 File file = File( fileName, "w" ); 125 file.writeln( contents ); 126 file.close; 127 } 128 129 /// Read a whole file into a string 130 string readFile( string path, string name ) { 131 auto fileName = path ~ "/" ~ name; 132 string content; 133 if (exists( fileName )) { 134 File file = File( fileName, "r" ); 135 foreach ( line; file.byLine()) 136 content ~= line; 137 } 138 return content; 139 } 140 141 /// git repository 142 struct GitRepo { 143 git_repository *repo; 144 145 /// Return the path of the repo 146 string workPath() { 147 return to!string( git_repository_workdir( repo ) ); 148 } 149 } 150 151 /// Open (or initializes when not exists) a repository in the given path 152 GitRepo openRepo( string repoPath ) { 153 GitRepo gr; 154 enforce( git_repository_init(&(gr.repo), repoPath.toStringz, 0) >= 0 ); 155 return gr; 156 } 157 158 /// Commit changes in the provided filename with the provided message 159 void commitChanges( GitRepo gr, string fileName, string message ) { 160 git_repository *repo = gr.repo; 161 git_index *my_repo_index; 162 163 enforce( git_repository_index(&my_repo_index, repo) >= 0 ); 164 165 //get last commit => parent 166 git_object* head; 167 int rc = git_revparse_single(&head, repo, "HEAD"); 168 169 if (rc == 0) { 170 // Check if there are actually any changes in the workdir 171 git_diff *diff; 172 enforce( git_diff_index_to_workdir( &diff, 173 repo, my_repo_index, null ) == 0 ); 174 if ( git_diff_num_deltas( diff ) == 0 ) { 175 debug writeln( "GIT: No changes: ", git_diff_num_deltas( diff ) ); 176 git_diff_free( diff ); 177 return; 178 } 179 git_diff_free( diff ); 180 } 181 182 enforce( git_index_add_bypath(my_repo_index,(fileName).toStringz) >= 0 ); 183 184 git_signature *sig; 185 enforce( git_signature_default(&sig, repo) >= 0 ); 186 187 188 git_oid tree_id, commit_id; 189 enforce( git_index_write( my_repo_index ) >= 0 ); 190 191 enforce( git_index_write_tree(&tree_id, my_repo_index) >= 0 ); 192 193 git_tree *tree; 194 enforce( git_tree_lookup(&tree, repo, &tree_id) >= 0, "Tree lookup failed" ); 195 196 if (rc<0) { // no head 197 debug writeln( "No head" ); 198 enforce( git_commit_create_v( 199 &commit_id, repo, "HEAD", sig, sig, 200 "UTF-8", "Initial commit", tree, 0 ) >=0 ); 201 } 202 else { 203 git_oid *parent_oid = cast(git_oid *)head; 204 git_commit* parent; 205 git_commit_lookup(&parent, repo, parent_oid); 206 207 enforce( git_commit_create_v( 208 &commit_id, repo, "HEAD", sig, sig, 209 "UTF-8", message.toStringz, tree, 1, parent ) >=0 ); 210 git_commit_free( parent ); 211 } 212 213 214 215 // Free everything 216 scope( exit ) { 217 git_index_free(my_repo_index); 218 git_signature_free(sig); 219 git_tree_free(tree); 220 } 221 } 222 223 // Not working correctly yet 224 void gitPush( GitRepo gr ) { 225 git_repository *repo = gr.repo; 226 git_remote *remote; 227 if ( git_remote_load( &remote, repo, "origin" ) == 0 ) { 228 enforce( git_remote_connect(remote, GIT_DIRECTION_PUSH) == 0, "Connection failed" ); 229 git_push *push; 230 enforce(git_push_new(&push, remote) == 0); 231 enforce(git_push_add_refspec(push, 232 "refs/heads/master:refs/heads/master") == 0 ); 233 enforce(git_push_finish(push) == 0); 234 git_remote_disconnect(remote); 235 enforce( git_remote_update_tips(remote) == 0); 236 } else { 237 debug writeln( "No remote found" ); 238 } 239 } 240 241 // Not working correctly yet 242 void gitPull( GitRepo gr ) { 243 git_repository *repo = gr.repo; 244 git_remote *remote; 245 if ( git_remote_load( &remote, repo, "origin") == 0 ) { 246 enforce( git_remote_fetch( remote ) == 0 ); // Get fetch head 247 git_object* fetch_head; 248 enforce( git_revparse_single(&fetch_head, repo, "FETCH_HEAD") == 0 ); 249 git_oid *fetch_head_id = cast(git_oid *)fetch_head; 250 git_merge_head *merge_fetch_head; 251 //git_merge_head_from_oid(&merge_fetch_head, repo, fetch_head_id); 252 enforce( git_merge_head_from_fetchhead(&merge_fetch_head, repo, "master", 253 "origin", fetch_head_id) == 0 ); 254 255 const(git_merge_head)* their_head = merge_fetch_head; 256 git_merge_result *result; 257 git_merge_opts *merge_opts; 258 size_t length = 1; 259 enforce( git_merge(&result, repo, &their_head, length, null) == 0 ); 260 } else { 261 debug writeln( "No remote found" ); 262 } 263 264 } 265 266 /// Add storage sub commands to the command list 267 Commands!( State delegate( State, string) ) addStorageCommands( 268 ref Commands!( State delegate( State, string) ) main, GitRepo gitRepo ) { 269 270 auto storageCommands = Commands!( State delegate( State, string) )("Commands specifically used to interact with stored config files"); 271 272 storageCommands.add( 273 "pull", delegate( State state, string parameter ) { 274 gitPull( gitRepo ); 275 return state; 276 }, "Pull todos from remote git repository" ); 277 278 storageCommands.add( 279 "push", delegate( State state, string parameter ) { 280 try { 281 gitPush( gitRepo ); 282 } catch (Throwable) { 283 writeln( "Git push failed. Did you set up a default remote called origin?" ); 284 } 285 return state; 286 }, "Push todos to remote git repository" ); 287 288 storageCommands.add( 289 "help", delegate( State state, string parameter ) { 290 state = main["clear"]( state, "" ); 291 writeln( storageCommands.toString ); 292 return state; 293 }, "Print this help message" ); 294 295 main.add( 296 "git", delegate( State state, string parameter ) { 297 auto split = parameter.findSplit( " " ); 298 state = storageCommands[split[0]]( state, split[2] ); 299 return state; 300 }, "Storage and git related commands. Use git help for more help." ); 301 302 main.addCompletion( "git", 303 delegate( string cmd, string parameter ) { 304 string[] results; 305 auto m = match( parameter, "^([A-z]*)$" ); 306 if (m) { 307 // Main commands 308 string[] command_keys = storageCommands.commands; 309 auto matching_commands = 310 filter!( a => match( a, m.captures[1] ))( command_keys ); 311 foreach ( com; matching_commands ) { 312 results ~= [cmd ~ " " ~ com]; 313 } 314 } 315 return results; 316 } ); 317 return main; 318 }