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 }