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 }