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 }