1/*
2 * This file is part of gitg
3 *
4 * Copyright (C) 2013 - Jesse van den Kieboom
5 *
6 * gitg is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * gitg is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with gitg. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20namespace Gitg
21{
22
23[Flags]
24public enum StageCommitOptions
25{
26	NONE       = 0,
27	SIGN_OFF   = 1 << 0,
28	AMEND      = 1 << 1,
29	SKIP_HOOKS = 1 << 2
30}
31
32public errordomain StageError
33{
34	PRE_COMMIT_HOOK_FAILED,
35	COMMIT_MSG_HOOK_FAILED,
36	NOTHING_TO_COMMIT,
37	INDEX_ENTRY_NOT_FOUND
38}
39
40public class PatchSet
41{
42	public enum Type
43	{
44		ADD    = 'a',
45		REMOVE = 'r'
46	}
47
48	public struct Patch
49	{
50		Type   type;
51		size_t old_offset;
52		size_t new_offset;
53		size_t length;
54	}
55
56	public string  filename;
57	public Patch[] patches;
58
59	public PatchSet reversed()
60	{
61		var ret = new PatchSet();
62
63		ret.filename = filename;
64		ret.patches = new Patch[patches.length];
65
66		for (int i = 0; i < patches.length; i++)
67		{
68			var orig = patches[i];
69
70			var p = Patch() {
71				old_offset = orig.new_offset,
72				new_offset = orig.old_offset,
73				length = orig.length
74			};
75
76			switch (patches[i].type)
77			{
78				case Type.ADD:
79					p.type = Type.REMOVE;
80					break;
81				case Type.REMOVE:
82					p.type = Type.ADD;
83					break;
84			}
85
86			ret.patches[i] = p;
87		}
88
89		return ret;
90	}
91}
92
93public class Stage : Object
94{
95	private weak Repository d_repository;
96	private Mutex d_index_mutex;
97	private Ggit.Tree? d_head_tree;
98
99	internal Stage(Repository repository)
100	{
101		d_repository = repository;
102	}
103
104	public async void refresh() throws Error
105	{
106		d_head_tree = null;
107
108		yield thread_index((index) => {
109			index.read(false);
110		});
111	}
112
113	public async Ggit.Tree? get_head_tree() throws Error
114	{
115		if (d_head_tree != null)
116		{
117			return d_head_tree;
118		}
119
120		Error? e = null;
121
122		yield Async.thread(() => {
123			try
124			{
125				var head = d_repository.get_head();
126				var commit = (Ggit.Commit)head.lookup();
127
128				d_head_tree = commit.get_tree();
129			}
130			catch (Error err)
131			{
132				e = err;
133			}
134		});
135
136		if (e != null)
137		{
138			throw e;
139		}
140
141		return d_head_tree;
142	}
143
144	public StageStatusEnumerator file_status(Ggit.StatusOptions? options = null)
145	{
146		return new StageStatusEnumerator(d_repository, options);
147	}
148
149	private delegate void WithIndexFunc(Ggit.Index index) throws Error;
150
151	private void with_index(WithIndexFunc func) throws Error
152	{
153		lock(d_index_mutex)
154		{
155			func(d_repository.get_index());
156		}
157	}
158
159	private async void thread_index(WithIndexFunc func) throws Error
160	{
161		yield Async.thread(() => {
162			with_index(func);
163		});
164	}
165
166	private string message_with_sign_off(string         message,
167	                                     Ggit.Signature committer)
168	{
169		return "%s\nSigned-off-by: %s <%s>\n".printf(message,
170		                                             committer.get_name(),
171		                                             committer.get_email());
172	}
173
174	private string convert_message_to_encoding(Ggit.Config conf,
175	                                           string      message,
176	                                           out string? encoding)
177	{
178		encoding = null;
179
180		try
181		{
182			encoding = conf.get_string("i18n.commitencoding");
183		}
184		catch
185		{
186			encoding = null;
187			return message;
188		}
189
190		if (encoding != null &&
191		    encoding != "" &&
192		    encoding.ascii_casecmp("UTF-8") != 0)
193		{
194			try
195			{
196				return convert(message, -1, encoding, "UTF-8");
197			}
198			catch
199			{
200				encoding = null;
201			}
202		}
203		else
204		{
205			encoding = null;
206		}
207
208		return message;
209	}
210
211	private void setup_commit_hook_environment(Gitg.Hook       hook,
212	                                           Ggit.Signature? author)
213	{
214		var wd = d_repository.get_workdir();
215		var gd = d_repository.get_location();
216
217		hook.working_directory = wd;
218
219		var gitdir = wd.get_relative_path(gd);
220
221		hook.environment["GIT_DIR"] = gitdir;
222		hook.environment["GIT_INDEX_FILE"] = Path.build_filename(gitdir, "index");
223		hook.environment["GIT_PREFIX"] = ".";
224
225		if (author != null)
226		{
227			hook.environment["GIT_AUTHOR_NAME"] = author.get_name();
228			hook.environment["GIT_AUTHOR_EMAIL"] = author.get_email();
229
230			var date = author.get_time();
231
232			var un = date.to_unix();
233			var tz = date.to_timezone(author.get_time_zone()).format("%z");
234
235			hook.environment["GIT_AUTHOR_DATE"] = @"@$(un) $(tz)";
236		}
237	}
238
239	public async void pre_commit_hook(Ggit.Signature author) throws StageError
240	{
241		string? errormsg = null;
242
243		try
244		{
245			yield Async.thread(() => {
246				// First run the pre-commit hook
247				var hook = new Gitg.Hook("pre-commit");
248
249				setup_commit_hook_environment(hook, author);
250
251				try
252				{
253					int status = hook.run_sync(d_repository);
254
255					if (status != 0)
256					{
257						errormsg = string.joinv("\n", hook.output);
258					}
259				}
260				catch (SpawnError e) {}
261			});
262		} catch {}
263
264		if (errormsg != null)
265		{
266			throw new StageError.PRE_COMMIT_HOOK_FAILED(errormsg);
267		}
268	}
269
270	private bool has_index_changes()
271	{
272		var show = Ggit.StatusShow.INDEX_ONLY;
273
274		var options = new Ggit.StatusOptions(0, show, null);
275		bool has_changes = false;
276
277		try
278		{
279			d_repository.file_status_foreach(options, (path, flags) => {
280				has_changes = true;
281				return -1;
282			});
283		} catch {}
284
285		return has_changes;
286	}
287
288	private string commit_msg_hook(string         message,
289	                               Ggit.Signature author,
290	                               Ggit.Signature committer) throws Error
291	{
292		var hook = new Gitg.Hook("commit-msg");
293
294		if (!hook.exists_in(d_repository))
295		{
296			return message;
297		}
298
299		setup_commit_hook_environment(hook, author);
300
301		var msgfile = d_repository.get_location().get_child("COMMIT_EDITMSG");
302
303		try
304		{
305			FileUtils.set_contents(msgfile.get_path(), message);
306		}
307		catch { return message; }
308
309		hook.add_argument(msgfile.get_path());
310
311		int status;
312
313		try
314		{
315			status = hook.run_sync(d_repository);
316		}
317		catch { return message; }
318
319		if (status != 0)
320		{
321			throw new StageError.COMMIT_MSG_HOOK_FAILED(string.joinv("\n", hook.output));
322		}
323
324		// Read back the message
325		try
326		{
327			string newmessage;
328
329			FileUtils.get_contents(msgfile.get_path(), out newmessage);
330			return newmessage;
331		}
332		catch (Error e)
333		{
334			throw new StageError.COMMIT_MSG_HOOK_FAILED(_("Could not read commit message after running commit-msg hook: %s").printf(e.message));
335		}
336		finally
337		{
338			FileUtils.remove(msgfile.get_path());
339		}
340	}
341
342	private void post_commit_hook(Ggit.Signature author)
343	{
344		var hook = new Gitg.Hook("post-commit");
345
346		setup_commit_hook_environment(hook, author);
347
348		hook.run.begin(d_repository, (obj, res) => {
349			try
350			{
351				hook.run.end(res);
352			} catch {}
353		});
354	}
355
356	private string get_subject(string message)
357	{
358		var nlpos = message.index_of("\n");
359
360		if (nlpos == -1)
361		{
362			return message;
363		}
364		else
365		{
366			return message[0:nlpos];
367		}
368	}
369
370	public async Ggit.OId? commit_index(Ggit.Index         index,
371	                                    Ggit.Ref           reference,
372	                                    string             message,
373	                                    Ggit.Signature     author,
374	                                    Ggit.Signature     committer,
375	                                    Ggit.OId[]?        parents,
376	                                    StageCommitOptions options) throws Error
377	{
378		Ggit.OId? treeoid = null;
379
380		yield Async.thread(() => {
381			treeoid = index.write_tree_to(d_repository);
382		});
383
384		return yield commit_tree(treeoid, reference, message, author, committer, parents, options);
385	}
386
387	public async Ggit.OId? commit_tree(Ggit.OId           treeoid,
388	                                   Ggit.Ref           reference,
389	                                   string             message,
390	                                   Ggit.Signature     author,
391	                                   Ggit.Signature     committer,
392	                                   Ggit.OId[]?        parents,
393	                                   StageCommitOptions options) throws Error
394	{
395		Ggit.OId? ret = null;
396
397		yield Async.thread(() => {
398			bool skip_hooks = (options & StageCommitOptions.SKIP_HOOKS) != 0;
399			bool amend = (options & StageCommitOptions.AMEND) != 0;
400
401			// Write tree from index
402			var conf = d_repository.get_config().snapshot();
403
404			string emsg = message;
405
406			if ((options & StageCommitOptions.SIGN_OFF) != 0)
407			{
408				emsg = message_with_sign_off(emsg, committer);
409			}
410
411			string? encoding;
412
413			emsg = convert_message_to_encoding(conf, emsg, out encoding);
414
415			if (!skip_hooks)
416			{
417				emsg = commit_msg_hook(emsg, author, committer);
418			}
419
420			Ggit.OId? refoid = null;
421
422			try
423			{
424				// Resolve the ref and get the actual target id
425				refoid = reference.resolve().get_target();
426			} catch {}
427
428			if (!amend)
429			{
430				Ggit.OId[] pars;
431
432				if (parents == null)
433				{
434					if (refoid == null)
435					{
436						pars = new Ggit.OId[] {};
437					}
438					else
439					{
440						pars = new Ggit.OId[] { refoid };
441					}
442				}
443				else
444				{
445					pars = parents;
446				}
447
448				ret = d_repository.create_commit_from_ids(reference.get_name(),
449				                                          author,
450				                                          committer,
451				                                          encoding,
452				                                          emsg,
453				                                          treeoid,
454				                                          pars);
455			}
456			else
457			{
458				var refcommit = d_repository.lookup<Ggit.Commit>(refoid);
459				var tree = d_repository.lookup<Ggit.Tree>(treeoid);
460
461				ret = refcommit.amend(reference.get_name(),
462				                       author,
463				                       committer,
464				                       encoding,
465				                       emsg,
466				                       tree);
467			}
468
469			bool always_update = false;
470
471			try
472			{
473				always_update = conf.get_bool("core.logAllRefUpdates");
474			} catch {}
475
476			string reflogmsg = "commit";
477
478			if (amend)
479			{
480				reflogmsg += " (amend)";
481			}
482
483			reflogmsg += ": " + get_subject(message);
484
485			// Update reflog of reference
486			try
487			{
488				if (always_update || reference.has_log())
489				{
490					var reflog = reference.get_log();
491					reflog.append(ret, committer, reflogmsg);
492					reflog.write();
493				}
494			} catch {}
495
496			if (reference.get_reference_type() == Ggit.RefType.SYMBOLIC)
497			{
498				// Update reflog of whereever HEAD points to
499				try
500				{
501					var resolved = reference.resolve();
502
503					if (always_update || resolved.has_log())
504					{
505						var reflog = resolved.get_log();
506
507						reflog.append(ret, committer, reflogmsg);
508						reflog.write();
509					}
510				} catch {}
511			}
512
513			if (reference.get_name() == "HEAD")
514			{
515				d_head_tree = null;
516			}
517
518			// run post commit
519			post_commit_hook(author);
520		});
521
522		return ret;
523	}
524
525	public async Ggit.OId? commit(string             message,
526	                              Ggit.Signature     author,
527	                              Ggit.Signature     committer,
528	                              StageCommitOptions options) throws Error
529	{
530		bool amend = (options & StageCommitOptions.AMEND) != 0;
531		Ggit.OId? ret = null;
532
533		lock(d_index_mutex)
534		{
535			Ggit.Index? index = null;
536
537			yield Async.thread(() => {
538				index = d_repository.get_index();
539			});
540
541			if (!amend && !has_index_changes())
542			{
543				throw new StageError.NOTHING_TO_COMMIT("Nothing to commit");
544			}
545
546			ret = yield commit_index(index,
547			                         d_repository.lookup_reference("HEAD"),
548			                         message,
549			                         author,
550			                         committer,
551			                         null,
552			                         options);
553		}
554
555		return ret;
556	}
557
558	/**
559	 * Revert working directory changes.
560	 *
561	 * @param file the file to revert.
562	 *
563	 * Revert a file to the version currently recorded in HEAD. This will delete
564	 * any modifications done in the current working directory to this file,
565	 * so use with care! Note that this only affects the working directory,
566	 * not the index.
567	 */
568	public async void revert(File file) throws Error
569	{
570		var tree = yield get_head_tree();
571
572		yield thread_index((index) => {
573			// get path relative to the repository working directory
574			var wd = d_repository.get_workdir();
575			var path = wd.get_relative_path(file);
576
577			// get the tree entry of that file
578			var entry = tree.get_by_path(path);
579			var id = entry.get_id();
580
581			// resolve the blob
582			var blob = d_repository.lookup<Ggit.Blob>(id);
583
584			var stream = file.replace(null, false, FileCreateFlags.NONE);
585
586			stream.write_all(blob.get_raw_content(), null);
587			stream.close();
588		});
589	}
590
591	/**
592	 * Revert working directory changes.
593	 *
594	 * @param path path relative to the working directory.
595	 *
596	 * Revert a path to the version currently recorded in HEAD. This will delete
597	 * any modifications done in the current working directory to this file,
598	 * so use with care! Note that this only affects the working directory,
599	 * not the index.
600	 */
601	public async void revert_path(string path) throws Error
602	{
603		yield revert(d_repository.get_workdir().resolve_relative_path(path));
604	}
605
606	/**
607	 * Revert a patch in the working directory.
608	 *
609	 * @param patch the patch to revert.
610	 *
611	 * Revert a provided patch from the working directory. The patch should
612	 * contain changes of the file in the current working directory to the contents
613	 * of the index (i.e. as obtained from diff_workdir)
614	 */
615	public async void revert_patch(PatchSet patch) throws Error
616	{
617		// new file is the current file in the working directory
618		var workdirf = d_repository.get_workdir().resolve_relative_path(patch.filename);
619		var workdirf_stream = yield workdirf.read_async();
620
621		yield thread_index((index) => {
622			var entries = index.get_entries();
623			var entry = entries.get_by_path(workdirf, 0);
624
625			if (entry == null)
626			{
627				throw new StageError.INDEX_ENTRY_NOT_FOUND(patch.filename);
628			}
629
630			var index_blob = d_repository.lookup<Ggit.Blob>(entry.get_id());
631			unowned uchar[] index_content = index_blob.get_raw_content();
632
633			var index_stream = new MemoryInputStream.from_bytes(new Bytes(index_content));
634			var reversed = patch.reversed();
635
636			FileIOStream? out_stream = null;
637			File ?outf = null;
638
639			try
640			{
641				outf = File.new_tmp(null, out out_stream);
642
643				apply_patch_stream(workdirf_stream,
644				                   index_stream,
645				                   out_stream.output_stream,
646				                   reversed);
647			}
648			catch (Error e)
649			{
650				workdirf_stream.close();
651				index_stream.close();
652
653				if (outf != null)
654				{
655					try
656					{
657						outf.delete();
658					} catch {}
659				}
660
661				throw e;
662			}
663
664			workdirf_stream.close();
665			index_stream.close();
666
667			if (out_stream != null)
668			{
669				out_stream.close();
670			}
671
672			// Move outf to workdirf
673			try
674			{
675				var repl = workdirf.replace(null,
676				                            false,
677				                            FileCreateFlags.NONE);
678
679				repl.splice(outf.read(),
680				            OutputStreamSpliceFlags.CLOSE_SOURCE |
681				            OutputStreamSpliceFlags.CLOSE_TARGET);
682			}
683			catch (Error e)
684			{
685				try
686				{
687					outf.delete();
688				} catch {}
689
690				throw e;
691			}
692
693			try
694			{
695				outf.delete();
696			} catch {}
697		});
698	}
699
700	/**
701	 * Delete a file from the index.
702	 *
703	 * @param file the file to delete.
704	 *
705	 * Delete the file from the index.
706	 */
707	public async void @delete(File file) throws Error
708	{
709		yield thread_index((index) => {
710			index.remove(file, 0);
711			index.write();
712		});
713	}
714
715	/**
716	 * Delete a relative path from the index.
717	 *
718	 * @param path path relative to the working directory.
719	 *
720	 * Delete the relative path from the index.
721	 */
722	public async void delete_path(string path) throws Error
723	{
724		yield this.delete(d_repository.get_workdir().resolve_relative_path(path));
725	}
726
727	/**
728	 * Stage a file to the index.
729	 *
730	 * @param file the file to stage.
731	 *
732	 * Stage the file to the index. This will record the state of the file in
733	 * the working directory to the index.
734	 */
735	public async void stage(File file) throws Error
736	{
737		yield thread_index((index) => {
738			index.add_file(file);
739			index.write();
740		});
741	}
742
743	/**
744	 * Stage a path to the index.
745	 *
746	 * @param path path relative to the working directory.
747	 *
748	 * Stage a relative path to the index. This will record the state of the file in
749	 * the working directory to the index.
750	 */
751	public async void stage_path(string path) throws Error
752	{
753		yield stage(d_repository.get_workdir().resolve_relative_path(path));
754	}
755
756	/**
757	 * Stage a commit to the index.
758	 *
759	 * @param path path relative to the working directory.
760	 * @param id the id of the commit object to stage at the given path.
761	 *
762	 * Stage a commit to the index with a relative path. This will record the
763	 * given commit id for file pointed to at path in the index.
764	 */
765	public async void stage_commit(string path, Ggit.Commit commit) throws Error
766	{
767		yield thread_index((index) => {
768			var entry = d_repository.create_index_entry_for_path(path, commit.get_id());
769			entry.set_commit(commit);
770
771			index.add(entry);
772			index.write();
773		});
774	}
775
776	private void copy_stream(OutputStream dest, InputStream src, ref size_t pos, size_t index, size_t length) throws Error
777	{
778		if (length == 0)
779		{
780			return;
781		}
782
783		var buf = new uint8[length];
784
785		if (pos != index)
786		{
787			((Seekable)src).seek(index, SeekType.SET);
788			pos = index;
789		}
790
791		src.read_all(buf, null);
792		dest.write_all(buf, null);
793
794		pos += length;
795	}
796
797	private void apply_patch(Ggit.Index  index,
798	                         InputStream old_stream,
799	                         InputStream new_stream,
800	                         PatchSet    patch) throws Error
801	{
802		var patched_stream = d_repository.create_blob();
803
804		apply_patch_stream(old_stream, new_stream, patched_stream, patch);
805
806		patched_stream.close();
807		var new_id = patched_stream.get_id();
808
809		var new_entry = d_repository.create_index_entry_for_path(patch.filename,
810		                                                         new_id);
811
812		index.add(new_entry);
813		index.write();
814	}
815
816	private void apply_patch_stream(InputStream  old_stream,
817	                                InputStream  new_stream,
818	                                OutputStream patched_stream,
819	                                PatchSet     patch) throws Error
820	{
821		size_t old_ptr = 0;
822		size_t new_ptr = 0;
823
824		// Copy old_content to patched_stream while applying patches as
825		// specified in patch.patches from new_stream
826		foreach (var p in patch.patches)
827		{
828			// Copy from old_ptr until p.old_offset
829			copy_stream(patched_stream,
830			            old_stream,
831			            ref old_ptr,
832			            old_ptr,
833			            p.old_offset - old_ptr);
834
835			if (p.type == PatchSet.Type.REMOVE)
836			{
837				// Removing, just advance old stream
838				((Seekable)old_stream).seek(p.length, SeekType.CUR);
839				old_ptr += p.length;
840			}
841			else
842			{
843				// Inserting, copy from new_stream
844				copy_stream(patched_stream,
845				            new_stream,
846				            ref new_ptr,
847				            p.new_offset,
848				            p.length);
849			}
850		}
851
852		// Copy remaining part of old
853		patched_stream.splice(old_stream, OutputStreamSpliceFlags.NONE);
854	}
855
856	/**
857	 * Stage a patch to the index.
858	 *
859	 * @param patch the patch to stage.
860	 *
861	 * Stage a provided patch to the index. The patch should contain changes
862	 * of the file in the current working directory to the contents of the
863	 * index (i.e. as obtained from diff_workdir)
864	 */
865	public async void stage_patch(PatchSet patch) throws Error
866	{
867		// new file is the current file in the working directory
868		var newf = d_repository.get_workdir().resolve_relative_path(patch.filename);
869		var new_stream = yield newf.read_async();
870
871		yield thread_index((index) => {
872			var entries = index.get_entries();
873			var entry = entries.get_by_path(newf, 0);
874
875			if (entry == null)
876			{
877				throw new StageError.INDEX_ENTRY_NOT_FOUND(patch.filename);
878			}
879
880			var old_blob = d_repository.lookup<Ggit.Blob>(entry.get_id());
881			unowned uchar[] old_content = old_blob.get_raw_content();
882
883			var old_stream = new MemoryInputStream.from_bytes(new Bytes(old_content));
884
885			apply_patch(index, old_stream, new_stream, patch);
886
887			new_stream.close();
888			old_stream.close();
889		});
890	}
891
892	/**
893	 * Unstage a file from the index.
894	 *
895	 * @param file the file to unstage.
896	 *
897	 * Unstage changes in the specified file from the index. This will record
898	 * the state of the file in HEAD to the index.
899	 */
900	public async void unstage(File file) throws Error
901	{
902		var tree = yield get_head_tree();
903
904		yield thread_index((index) => {
905			// get path relative to the repository working directory
906			var wd = d_repository.get_workdir();
907			var path = wd.get_relative_path(file);
908
909			// get the tree entry of that file
910			var entry = tree.get_by_path(path);
911			var id = entry.get_id();
912
913			// create a new index entry for the file, pointing to the blob
914			// from the HEAD tree
915			var ientry = d_repository.create_index_entry_for_path(path, id);
916
917			// override file mode since the file might not actually exist.
918			ientry.set_mode(entry.get_file_mode());
919
920			index.add(ientry);
921			index.write();
922		});
923	}
924
925	/**
926	 * Unstage a path from the index.
927	 *
928	 * @param path path relative to the working directory.
929	 *
930	 * Unstage changes in the specified relative path from the index. This will record
931	 * the state of the file in HEAD to the index.
932	 */
933	public async void unstage_path(string path) throws Error
934	{
935		yield unstage(d_repository.get_workdir().resolve_relative_path(path));
936	}
937
938	/**
939	 * Unstage a patch from the index.
940	 *
941	 * @param patch the patch to unstage.
942	 *
943	 * Unstage a provided patch from the index. The patch should contain changes
944	 * of the file in the index to the file in HEAD.
945	 */
946	public async void unstage_patch(PatchSet patch) throws Error
947	{
948		var file = d_repository.get_workdir().resolve_relative_path(patch.filename);
949		var tree = yield get_head_tree();
950
951		yield thread_index((index) => {
952			var entries = index.get_entries();
953			var entry = entries.get_by_path(file, 0);
954
955			if (entry == null)
956			{
957				throw new StageError.INDEX_ENTRY_NOT_FOUND(patch.filename);
958			}
959
960			var head_entry = tree.get_by_path(patch.filename);
961			var head_blob = d_repository.lookup<Ggit.Blob>(head_entry.get_id());
962			var index_blob = d_repository.lookup<Ggit.Blob>(entry.get_id());
963
964			unowned uchar[] head_content = head_blob.get_raw_content();
965			unowned uchar[] index_content = index_blob.get_raw_content();
966
967			var head_stream = new MemoryInputStream.from_bytes(new Bytes(head_content));
968			var index_stream = new MemoryInputStream.from_bytes(new Bytes(index_content));
969
970			var reversed = patch.reversed();
971
972			apply_patch(index, index_stream, head_stream, reversed);
973
974			head_stream.close();
975			index_stream.close();
976		});
977	}
978
979	public async Ggit.Diff? diff_index_all(StageStatusItem[]? files,
980	                                       Ggit.DiffOptions?  defopts = null) throws Error
981	{
982		var opts = new Ggit.DiffOptions();
983
984		opts.flags = Ggit.DiffOption.INCLUDE_UNTRACKED |
985		             Ggit.DiffOption.DISABLE_PATHSPEC_MATCH |
986		             Ggit.DiffOption.RECURSE_UNTRACKED_DIRS;
987
988
989		if (files != null)
990		{
991			var pspec = new string[files.length];
992
993			for (var i = 0; i < files.length; i++)
994			{
995				pspec[i] = files[i].path;
996			}
997
998			opts.pathspec = pspec;
999		}
1000
1001		if (defopts != null)
1002		{
1003			opts.flags |= defopts.flags;
1004
1005			opts.n_context_lines = defopts.n_context_lines;
1006			opts.n_interhunk_lines = defopts.n_interhunk_lines;
1007
1008			opts.old_prefix = defopts.old_prefix;
1009			opts.new_prefix = defopts.new_prefix;
1010		}
1011
1012		Ggit.Tree? tree = null;
1013
1014		if (!d_repository.is_empty())
1015		{
1016			tree = yield get_head_tree();
1017		}
1018
1019		return new Ggit.Diff.tree_to_index(d_repository,
1020		                                   tree,
1021		                                   d_repository.get_index(),
1022		                                   opts);
1023	}
1024
1025	public async Ggit.Diff? diff_index(StageStatusItem   f,
1026	                                   Ggit.DiffOptions? defopts = null) throws Error
1027	{
1028		return yield diff_index_all(new StageStatusItem[] {f}, defopts);
1029	}
1030
1031	public async Ggit.Diff? diff_workdir_all(StageStatusItem[] files,
1032	                                         Ggit.DiffOptions? defopts = null) throws Error
1033	{
1034		var opts = new Ggit.DiffOptions();
1035
1036		opts.flags = Ggit.DiffOption.INCLUDE_UNTRACKED |
1037		             Ggit.DiffOption.DISABLE_PATHSPEC_MATCH |
1038		             Ggit.DiffOption.RECURSE_UNTRACKED_DIRS |
1039		             Ggit.DiffOption.SHOW_UNTRACKED_CONTENT;
1040
1041		if (files != null)
1042		{
1043			var pspec = new string[files.length];
1044
1045			for (var i = 0; i < files.length; i++)
1046			{
1047				pspec[i] = files[i].path;
1048			}
1049
1050			opts.pathspec = pspec;
1051		}
1052
1053		if (defopts != null)
1054		{
1055			opts.flags |= defopts.flags;
1056
1057			opts.n_context_lines = defopts.n_context_lines;
1058			opts.n_interhunk_lines = defopts.n_interhunk_lines;
1059
1060			opts.old_prefix = defopts.old_prefix;
1061			opts.new_prefix = defopts.new_prefix;
1062		}
1063
1064		return new Ggit.Diff.index_to_workdir(d_repository,
1065		                                      d_repository.get_index(),
1066		                                      opts);
1067	}
1068
1069	public async Ggit.Diff? diff_workdir(StageStatusItem   f,
1070	                                     Ggit.DiffOptions? defopts = null) throws Error
1071	{
1072		return yield diff_workdir_all(new StageStatusItem[] {f}, defopts);
1073	}
1074}
1075
1076}
1077
1078// ex:set ts=4 noet
1079