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