1 module todod.storage; 2 3 import std.algorithm; 4 import std.conv; 5 import std.datetime : Clock, SysTime; 6 import std.exception; 7 import std.file; 8 import std.regex; 9 import std.stdio; 10 import std.string; 11 import std.uuid; 12 13 import deimos.git2.all; 14 15 import todod.commandline; 16 import todod.state; 17 18 struct FileWatcher 19 { 20 this( string path, string file ) 21 { 22 _path = path; 23 _file = file; 24 25 if (_path[$-1] != '/') 26 _path ~= "/"; 27 28 watchCreated = Clock.currTime(); 29 } 30 31 bool changed() 32 { 33 if ( DirEntry( _path ~ _file ).timeLastModified() > watchCreated ) 34 return true; 35 return false; 36 } 37 38 private: 39 string _path; 40 string _file; 41 SysTime watchCreated; 42 } 43 44 unittest 45 { 46 // Test that FileWatcher works with path with and without trailing 47 auto fw = FileWatcher( "path/", "file" ); 48 assert( fw._path == "path/" && fw._file == "file" ); 49 fw = FileWatcher( "path", "file" ); 50 assert( fw._path == "path/" && fw._file == "file" ); 51 import std.datetime; 52 fw.watchCreated -= 1000.msecs; // Minimum precision is one second 53 54 mkdirRecurse( "path/" ); 55 // Change/touch file 56 File( "path/file", "w" ).close; 57 // Check that filewatcher knows it has been changed 58 assert( fw.changed ); 59 60 // Cleanup 61 rmdirRecurse( "path" ); 62 } 63 64 /// Write string contents to file at the given path 65 void writeToFile( string path, string name, string contents ) { 66 auto fileName = path ~ "/" ~ name; 67 File file = File( fileName, "w" ); 68 file.writeln( contents ); 69 file.close; 70 } 71 72 /// Read a whole file into a string 73 string readFile( string path, string name ) { 74 auto fileName = path ~ "/" ~ name; 75 string content; 76 if (exists( fileName )) { 77 File file = File( fileName, "r" ); 78 foreach ( line; file.byLine()) 79 content ~= line; 80 } 81 return content; 82 } 83 84 /// git repository 85 struct GitRepo { 86 git_repository *repo; 87 88 /// Return the path of the repo 89 string workPath() { 90 return to!string( git_repository_workdir( repo ) ); 91 } 92 } 93 94 /// Open (or initializes when not exists) a repository in the given path 95 GitRepo openRepo( string repoPath ) { 96 GitRepo gr; 97 enforce( git_repository_init(&(gr.repo), repoPath.toStringz, 0) >= 0 ); 98 return gr; 99 } 100 101 /// Commit changes in the provided filename with the provided message 102 void commitChanges( GitRepo gr, string fileName, string message ) { 103 git_repository *repo = gr.repo; 104 git_index *my_repo_index; 105 106 enforce( git_repository_index(&my_repo_index, repo) >= 0 ); 107 108 //get last commit => parent 109 git_object* head; 110 int rc = git_revparse_single(&head, repo, "HEAD"); 111 112 if (rc == 0) { 113 // Check if there are actually any changes in the workdir 114 git_diff *diff; 115 enforce( git_diff_index_to_workdir( &diff, 116 repo, my_repo_index, null ) == 0 ); 117 if ( git_diff_num_deltas( diff ) == 0 ) { 118 debug writeln( "GIT: No changes: ", git_diff_num_deltas( diff ) ); 119 git_diff_free( diff ); 120 return; 121 } 122 git_diff_free( diff ); 123 } 124 125 enforce( git_index_add_bypath(my_repo_index,(fileName).toStringz) >= 0 ); 126 127 git_signature *sig; 128 enforce( git_signature_default(&sig, repo) >= 0 ); 129 130 131 git_oid tree_id, commit_id; 132 enforce( git_index_write( my_repo_index ) >= 0 ); 133 134 enforce( git_index_write_tree(&tree_id, my_repo_index) >= 0 ); 135 136 git_tree *tree; 137 enforce( git_tree_lookup(&tree, repo, &tree_id) >= 0, "Tree lookup failed" ); 138 139 if (rc<0) { // no head 140 debug writeln( "No head" ); 141 enforce( git_commit_create_v( 142 &commit_id, repo, "HEAD", sig, sig, 143 "UTF-8", "Initial commit", tree, 0 ) >=0 ); 144 } 145 else { 146 git_oid *parent_oid = cast(git_oid *)head; 147 git_commit* parent; 148 git_commit_lookup(&parent, repo, parent_oid); 149 150 enforce( git_commit_create_v( 151 &commit_id, repo, "HEAD", sig, sig, 152 "UTF-8", message.toStringz, tree, 1, parent ) >=0 ); 153 git_commit_free( parent ); 154 } 155 156 157 158 // Free everything 159 scope( exit ) { 160 git_index_free(my_repo_index); 161 git_signature_free(sig); 162 git_tree_free(tree); 163 } 164 } 165 166 // Not working correctly yet 167 void gitPush( GitRepo gr ) { 168 git_repository *repo = gr.repo; 169 git_remote *remote; 170 if ( git_remote_load( &remote, repo, "origin" ) == 0 ) { 171 enforce( git_remote_connect(remote, GIT_DIRECTION_PUSH) == 0, "Connection failed" ); 172 git_push *push; 173 enforce(git_push_new(&push, remote) == 0); 174 enforce(git_push_add_refspec(push, 175 "refs/heads/master:refs/heads/master") == 0 ); 176 enforce(git_push_finish(push) == 0); 177 git_remote_disconnect(remote); 178 enforce( git_remote_update_tips(remote) == 0); 179 } else { 180 debug writeln( "No remote found" ); 181 } 182 } 183 184 // Not working correctly yet 185 void gitPull( GitRepo gr ) { 186 git_repository *repo = gr.repo; 187 git_remote *remote; 188 if ( git_remote_load( &remote, repo, "origin") == 0 ) { 189 enforce( git_remote_fetch( remote ) == 0 ); // Get fetch head 190 git_object* fetch_head; 191 enforce( git_revparse_single(&fetch_head, repo, "FETCH_HEAD") == 0 ); 192 git_oid *fetch_head_id = cast(git_oid *)fetch_head; 193 git_merge_head *merge_fetch_head; 194 //git_merge_head_from_oid(&merge_fetch_head, repo, fetch_head_id); 195 enforce( git_merge_head_from_fetchhead(&merge_fetch_head, repo, "master", 196 "origin", fetch_head_id) == 0 ); 197 198 const(git_merge_head)* their_head = merge_fetch_head; 199 git_merge_result *result; 200 git_merge_opts *merge_opts; 201 size_t length = 1; 202 enforce( git_merge(&result, repo, &their_head, length, null) == 0 ); 203 } else { 204 debug writeln( "No remote found" ); 205 } 206 207 } 208 209 /// Add storage sub commands to the command list 210 Commands!( State delegate( State, string) ) addStorageCommands( 211 ref Commands!( State delegate( State, string) ) main, GitRepo gitRepo ) { 212 213 auto storageCommands = Commands!( State delegate( State, string) )("Commands specifically used to interact with stored config files"); 214 215 storageCommands.add( 216 "pull", delegate( State state, string parameter ) { 217 gitPull( gitRepo ); 218 return state; 219 }, "Pull todos from remote git repository" ); 220 221 storageCommands.add( 222 "push", delegate( State state, string parameter ) { 223 try { 224 gitPush( gitRepo ); 225 } catch (Throwable) { 226 writeln( "Git push failed. Did you set up a default remote called origin?" ); 227 } 228 return state; 229 }, "Push todos to remote git repository" ); 230 231 storageCommands.add( 232 "help", delegate( State state, string parameter ) { 233 state = main["clear"]( state, "" ); 234 writeln( storageCommands.toString ); 235 return state; 236 }, "Print this help message" ); 237 238 main.add( 239 "git", delegate( State state, string parameter ) { 240 auto split = parameter.findSplit( " " ); 241 state = storageCommands[split[0]]( state, split[2] ); 242 return state; 243 }, "Storage and git related commands. Use git help for more help." ); 244 245 main.addCompletion( "git", 246 delegate( string cmd, string parameter ) { 247 string[] results; 248 auto m = match( parameter, "^([A-z]*)$" ); 249 if (m) { 250 // Main commands 251 string[] command_keys = storageCommands.commands; 252 auto matching_commands = 253 filter!( a => match( a, m.captures[1] ))( command_keys ); 254 foreach ( com; matching_commands ) { 255 results ~= [cmd ~ " " ~ com]; 256 } 257 } 258 return results; 259 } ); 260 return main; 261 }