gitg/libgitg/gitg-stage.vala

639 lines
14 KiB
Vala
Raw Normal View History

/*
* This file is part of gitg
*
* Copyright (C) 2013 - Jesse van den Kieboom
*
* gitg is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* gitg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with gitg. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Gitg
{
2013-07-04 08:28:06 +00:00
[Flags]
public enum StageCommitOptions
{
2013-07-05 11:05:01 +00:00
NONE = 0,
SIGN_OFF = 1 << 0,
AMEND = 1 << 1,
SKIP_HOOKS = 1 << 2
}
public errordomain StageError
{
PRE_COMMIT_HOOK_FAILED,
COMMIT_MSG_HOOK_FAILED,
NOTHING_TO_COMMIT
2013-07-04 08:28:06 +00:00
}
2013-06-29 12:44:01 +00:00
public class Stage : Object
{
2013-07-03 14:40:48 +00:00
private weak Repository d_repository;
private Mutex d_index_mutex;
2013-07-03 14:41:22 +00:00
private Ggit.Tree? d_head_tree;
internal Stage(Repository repository)
{
d_repository = repository;
}
public async void refresh() throws Error
{
yield thread_index((index) => {
index.read();
});
}
2013-07-03 14:41:22 +00:00
public async Ggit.Tree? get_head_tree() throws Error
{
2013-07-03 14:41:22 +00:00
if (d_head_tree != null)
2013-06-29 12:45:07 +00:00
{
2013-07-03 14:41:22 +00:00
return d_head_tree;
2013-06-29 12:45:07 +00:00
}
2013-07-03 14:41:22 +00:00
Error? e = null;
yield Async.thread(() => {
try
{
var head = d_repository.get_head();
var commit = (Ggit.Commit)head.lookup();
d_head_tree = commit.get_tree();
}
catch (Error err)
{
e = err;
}
});
if (e != null)
{
throw e;
}
return d_head_tree;
}
public StageStatusEnumerator file_status(Ggit.StatusOptions? options = null)
{
return new StageStatusEnumerator(d_repository, options);
}
private delegate void WithIndexFunc(Ggit.Index index) throws Error;
private void with_index(WithIndexFunc func) throws Error
{
lock(d_index_mutex)
{
func(d_repository.get_index());
}
}
private async void thread_index(WithIndexFunc func) throws Error
{
yield Async.thread(() => {
with_index(func);
});
}
2013-07-05 11:05:01 +00:00
private string message_with_sign_off(string message,
Ggit.Signature committer)
2013-07-04 08:28:06 +00:00
{
2013-07-05 11:05:01 +00:00
return "%s\nSigned-off-by: %s <%s>\n".printf(message,
committer.get_name(),
committer.get_email());
2013-07-04 08:28:06 +00:00
}
2013-07-05 11:05:01 +00:00
private string convert_message_to_encoding(Ggit.Config conf,
string message,
out string? encoding)
2013-07-04 08:28:06 +00:00
{
2013-07-05 11:05:01 +00:00
encoding = null;
2013-07-04 08:28:06 +00:00
try
{
encoding = conf.get_string("i18n.commitencoding");
}
catch
{
2013-07-05 11:05:01 +00:00
encoding = null;
2013-07-04 08:28:06 +00:00
return message;
}
if (encoding != null &&
encoding != "" &&
encoding.ascii_casecmp("UTF-8") != 0)
{
try
{
return convert(message, -1, encoding, "UTF-8");
}
2013-07-05 11:05:01 +00:00
catch
{
encoding = null;
}
}
else
{
encoding = null;
2013-07-04 08:28:06 +00:00
}
return message;
}
2013-07-05 11:05:01 +00:00
private void setup_commit_hook_environment(Gitg.Hook hook,
Ggit.Signature? author)
2013-07-04 08:28:06 +00:00
{
2013-07-05 11:05:01 +00:00
var wd = d_repository.get_workdir();
var gd = d_repository.get_location();
2013-07-04 08:28:06 +00:00
2013-07-05 11:05:01 +00:00
hook.working_directory = wd;
var gitdir = wd.get_relative_path(gd);
hook.environment["GIT_DIR"] = gitdir;
hook.environment["GIT_INDEX_FILE"] = Path.build_filename(gitdir, "index");
hook.environment["GIT_PREFIX"] = ".";
if (author != null)
{
hook.environment["GIT_AUTHOR_NAME"] = author.get_name();
hook.environment["GIT_AUTHOR_EMAIL"] = author.get_email();
2013-07-06 10:23:39 +00:00
var date = author.get_time();
var un = date.to_unix();
var tz = date.to_timezone(author.get_time_zone()).format("%z");
hook.environment["GIT_AUTHOR_DATE"] = @"@$(un) $(tz)";
2013-07-05 11:05:01 +00:00
}
}
public async void pre_commit_hook(Ggit.Signature author) throws StageError
{
string? errormsg = null;
try
{
yield Async.thread(() => {
// First run the pre-commit hook
var hook = new Gitg.Hook("pre-commit");
2013-07-05 11:05:01 +00:00
setup_commit_hook_environment(hook, author);
2013-07-05 11:05:01 +00:00
try
2013-07-05 13:45:46 +00:00
{
int status = hook.run_sync(d_repository);
if (status != 0)
{
errormsg = string.joinv("\n", hook.output);
}
2013-07-05 11:05:01 +00:00
}
catch (SpawnError e) {}
});
} catch {}
2013-07-05 11:05:01 +00:00
if (errormsg != null)
{
throw new StageError.PRE_COMMIT_HOOK_FAILED(errormsg);
}
}
private bool has_index_changes()
{
var opts = Ggit.StatusOption.EXCLUDE_SUBMODULES;
var show = Ggit.StatusShow.INDEX_ONLY;
var options = new Ggit.StatusOptions(opts, show, null);
bool has_changes = false;
try
{
d_repository.file_status_foreach(options, (path, flags) => {
has_changes = true;
return -1;
});
} catch {}
return has_changes;
}
private string commit_msg_hook(string message,
Ggit.Signature author,
Ggit.Signature committer) throws Error
2013-07-05 11:05:01 +00:00
{
var hook = new Gitg.Hook("commit-msg");
if (!hook.exists_in(d_repository))
{
return message;
}
setup_commit_hook_environment(hook, author);
var msgfile = d_repository.get_location().get_child("COMMIT_EDITMSG");
try
{
FileUtils.set_contents(msgfile.get_path(), message);
}
catch { return message; }
2013-07-04 08:28:06 +00:00
2013-07-05 11:05:01 +00:00
hook.add_argument(msgfile.get_path());
2013-07-04 08:28:06 +00:00
2013-07-05 11:05:01 +00:00
int status;
try
{
status = hook.run_sync(d_repository);
}
catch { return message; }
if (status != 0)
{
throw new StageError.COMMIT_MSG_HOOK_FAILED(string.joinv("\n", hook.output));
}
// Read back the message
try
{
string newmessage;
FileUtils.get_contents(msgfile.get_path(), out newmessage);
return newmessage;
}
catch (Error e)
{
throw new StageError.COMMIT_MSG_HOOK_FAILED(_("Could not read commit message after running commit-msg hook: %s").printf(e.message));
}
finally
{
FileUtils.remove(msgfile.get_path());
}
}
private void post_commit_hook(Ggit.Signature author)
{
var hook = new Gitg.Hook("post-commit");
setup_commit_hook_environment(hook, author);
hook.run.begin(d_repository, (obj, res) => {
try
{
hook.run.end(res);
} catch {}
});
}
private string get_subject(string message)
{
var nlpos = message.index_of("\n");
if (nlpos == -1)
{
return message;
}
else
{
return message[0:nlpos];
}
}
public async Ggit.OId? commit(string message,
Ggit.Signature author,
Ggit.Signature committer,
StageCommitOptions options) throws Error
{
Ggit.OId? ret = null;
bool skip_hooks = (options & StageCommitOptions.SKIP_HOOKS) != 0;
bool amend = (options & StageCommitOptions.AMEND) != 0;
2013-07-05 11:05:01 +00:00
yield thread_index((index) => {
if (!amend && !has_index_changes())
2013-07-05 11:05:01 +00:00
{
throw new StageError.NOTHING_TO_COMMIT("Nothing to commit");
}
// Write tree from index
2013-07-04 08:28:06 +00:00
var conf = d_repository.get_config();
string emsg = message;
if ((options & StageCommitOptions.SIGN_OFF) != 0)
{
2013-07-05 11:05:01 +00:00
emsg = message_with_sign_off(emsg, committer);
}
string? encoding;
emsg = convert_message_to_encoding(conf, emsg, out encoding);
if (!skip_hooks)
{
emsg = commit_msg_hook(emsg, author, committer);
2013-07-04 08:28:06 +00:00
}
2013-07-05 11:05:01 +00:00
var treeoid = index.write_tree();
2013-07-05 13:46:00 +00:00
// Note: get the symbolic ref here
var head = d_repository.lookup_reference("HEAD");
2013-07-06 11:33:53 +00:00
Ggit.OId? headoid = null;
try
{
// Resolve the ref and get the actual target id
headoid = head.resolve().get_target();
} catch {}
Ggit.OId[] parents;
if (headoid == null)
{
parents = new Ggit.OId[] {};
}
else
{
if (amend)
2013-07-06 11:33:53 +00:00
{
var commit = d_repository.lookup<Ggit.Commit>(headoid);
var p = commit.get_parents();
parents = new Ggit.OId[p.size()];
for (int i = 0; i < p.size(); ++i)
{
parents[i] = p.get_id(i);
}
}
else
{
parents = new Ggit.OId[] { headoid };
}
}
2013-07-04 08:28:06 +00:00
2013-07-05 11:05:01 +00:00
ret = d_repository.create_commit_from_oids("HEAD",
author,
committer,
encoding,
emsg,
treeoid,
2013-07-06 11:33:53 +00:00
parents);
2013-07-04 08:28:06 +00:00
2013-07-05 13:46:00 +00:00
bool always_update = false;
try
{
always_update = conf.get_bool("core.logAllRefUpdates");
} catch {}
string reflogmsg = "commit";
if (amend)
{
reflogmsg += " (amend)";
}
reflogmsg += ": " + get_subject(message);
2013-07-05 13:46:00 +00:00
// Update reflog of HEAD
try
{
if (always_update || head.has_reflog())
{
var reflog = head.get_reflog();
reflog.append(ret, committer, reflogmsg);
reflog.write();
}
} catch {}
if (head.get_reference_type() == Ggit.RefType.SYMBOLIC)
2013-07-05 11:05:01 +00:00
{
2013-07-05 13:46:00 +00:00
// Update reflog of whereever HEAD points to
2013-07-05 11:05:01 +00:00
try
{
2013-07-05 13:46:00 +00:00
var resolved = head.resolve();
if (always_update || resolved.has_reflog())
{
var reflog = resolved.get_reflog();
reflog.append(ret, committer, reflogmsg);
reflog.write();
}
2013-07-05 11:05:01 +00:00
} catch {}
}
2013-07-04 08:28:06 +00:00
2013-07-05 11:05:01 +00:00
// run post commit
post_commit_hook(author);
2013-07-04 08:28:06 +00:00
});
2013-07-05 11:05:01 +00:00
return ret;
2013-07-04 08:28:06 +00:00
}
/**
* Revert working directory changes.
*
* @param file the file to revert.
*
* Revert a file to the version currently recorded in HEAD. This will delete
* any modifications done in the current working directory to this file,
* so use with care! Note that this only affects the working directory,
* not the index.
*/
public async void revert(File file) throws Error
{
2013-07-03 14:41:22 +00:00
var tree = yield get_head_tree();
2013-07-03 14:41:22 +00:00
yield thread_index((index) => {
// get path relative to the repository working directory
var wd = d_repository.get_workdir();
var path = wd.get_relative_path(file);
// get the tree entry of that file
var entry = tree.get_by_path(path);
var id = entry.get_id();
// resolve the blob
var blob = d_repository.lookup<Ggit.Blob>(id);
var stream = file.replace(null, false, FileCreateFlags.NONE);
2013-07-03 14:41:22 +00:00
stream.write_all(blob.get_raw_content(), null);
stream.close();
index.write();
});
}
/**
* Revert working directory changes.
*
* @param path path relative to the working directory.
*
* Revert a path to the version currently recorded in HEAD. This will delete
* any modifications done in the current working directory to this file,
* so use with care! Note that this only affects the working directory,
* not the index.
*/
public async void revert_path(string path) throws Error
{
yield revert(d_repository.get_workdir().resolve_relative_path(path));
}
/**
* Delete a file from the index.
*
* @param file the file to delete.
*
* Delete the file from the index.
*/
public async void @delete(File file) throws Error
{
yield thread_index((index) => {
index.remove(file, 0);
index.write();
});
}
/**
* Delete a relative path from the index.
*
* @param path path relative to the working directory.
*
* Delete the relative path from the index.
*/
public async void delete_path(string path) throws Error
{
yield this.delete(d_repository.get_workdir().resolve_relative_path(path));
}
/**
* Stage a file to the index.
*
* @param file the file to stage.
*
* Stage the file to the index. This will record the state of the file in
* the working directory to the index.
*/
public async void stage(File file) throws Error
{
yield thread_index((index) => {
index.add_file(file);
index.write();
});
}
/**
* Stage a path to the index.
*
* @param path path relative to the working directory.
*
* Stage a relative path to the index. This will record the state of the file in
* the working directory to the index.
*/
public async void stage_path(string path) throws Error
{
yield stage(d_repository.get_workdir().resolve_relative_path(path));
}
/**
* Unstage a file from the index.
*
* @param file the file to unstage.
*
* Unstage changes in the specified file from the index. This will record
* the state of the file in HEAD to the index.
*/
public async void unstage(File file) throws Error
{
2013-07-05 11:05:01 +00:00
var tree = yield get_head_tree();
2013-07-05 11:05:01 +00:00
yield thread_index((index) => {
// get path relative to the repository working directory
var wd = d_repository.get_workdir();
var path = wd.get_relative_path(file);
// get the tree entry of that file
var entry = tree.get_by_path(path);
var id = entry.get_id();
// create a new index entry for the file, pointing to the blob
// from the HEAD tree
var ientry = d_repository.create_index_entry_for_path(path, id);
// override file mode since the file might not actually exist.
ientry.set_mode(entry.get_file_mode());
index.add(ientry);
index.write();
});
}
/**
* Unstage a path from the index.
*
* @param path path relative to the working directory.
*
* Unstage changes in the specified relative path from the index. This will record
* the state of the file in HEAD to the index.
*/
public async void unstage_path(string path) throws Error
{
yield unstage(d_repository.get_workdir().resolve_relative_path(path));
}
2013-11-23 13:54:29 +00:00
public async Ggit.Diff? diff_index(StageStatusFile f) throws Error
{
2013-11-23 13:54:29 +00:00
var opts = new Ggit.DiffOptions(Ggit.DiffOption.INCLUDE_UNTRACKED |
Ggit.DiffOption.DISABLE_PATHSPEC_MATCH |
Ggit.DiffOption.RECURSE_UNTRACKED_DIRS,
3,
3,
null,
null,
new string[] {f.path});
var tree = yield get_head_tree();
2013-11-23 13:54:29 +00:00
return new Ggit.Diff.tree_to_index(d_repository,
tree,
d_repository.get_index(),
opts);
}
2013-11-23 13:54:29 +00:00
public async Ggit.Diff? diff_workdir(StageStatusFile f) throws Error
{
2013-11-23 13:54:29 +00:00
var opts = new Ggit.DiffOptions(Ggit.DiffOption.INCLUDE_UNTRACKED |
Ggit.DiffOption.DISABLE_PATHSPEC_MATCH |
Ggit.DiffOption.RECURSE_UNTRACKED_DIRS,
3,
3,
null,
null,
new string[] {f.path});
2013-11-23 13:54:29 +00:00
return new Ggit.Diff.index_to_workdir(d_repository,
d_repository.get_index(),
opts);
}
}
}
// ex:set ts=4 noet