SparkleShare/Sparkles/Git/Git.Repository.cs

1109 lines
40 KiB
C#
Raw Permalink Normal View History

// SparkleShare, a collaboration and sharing tool.
2017-07-23 12:47:54 +00:00
// Copyright (C) 2010 Hylke Bons <hi@planetpeanut.uk>
//
// This program is free software: you can redistribute it and/or modify
2018-04-09 03:41:59 +00:00
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the
2013-10-11 15:13:46 +00:00
// License, or (at your option) any later version.
//
// This program 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
2013-10-11 15:13:46 +00:00
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
2011-03-08 23:55:21 +00:00
2016-03-31 08:35:26 +00:00
namespace Sparkles.Git {
2016-03-30 23:36:31 +00:00
public class GitRepository : BaseRepository {
2011-05-18 18:12:45 +00:00
SSHAuthenticationInfo auth_info;
bool user_is_set;
string cached_branch;
2012-11-18 18:49:57 +00:00
string branch {
get {
2018-04-09 03:41:59 +00:00
if (!string.IsNullOrEmpty (this.cached_branch))
2012-11-28 20:17:39 +00:00
return this.cached_branch;
var git = new GitCommand (LocalPath, "config core.ignorecase true");
git.StartAndWaitForExit ();
2016-06-18 00:03:40 +00:00
// TODO: ugly
while (this.in_merge && HasLocalChanges) {
2012-11-28 20:17:39 +00:00
try {
ResolveConflict ();
2018-04-09 03:41:59 +00:00
2012-11-28 20:17:39 +00:00
} catch (IOException e) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Failed to resolve conflict, trying again...", e);
2012-11-28 20:17:39 +00:00
}
}
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "config core.ignorecase false");
git.StartAndWaitForExit ();
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "rev-parse --abbrev-ref HEAD");
2012-11-28 20:17:39 +00:00
this.cached_branch = git.StartAndReadStandardOutput ();
return this.cached_branch;
}
}
bool in_merge {
get {
2016-03-31 14:46:17 +00:00
string merge_file_path = Path.Combine (LocalPath, ".git", "MERGE_HEAD");
return File.Exists (merge_file_path);
}
}
public GitRepository (string path, Configuration config, SSHAuthenticationInfo auth_info) : base (path, config)
2012-01-29 20:33:12 +00:00
{
this.auth_info = auth_info;
2016-05-30 03:14:40 +00:00
var git_config = new GitCommand (LocalPath, "config core.ignorecase false");
git_config.StartAndWaitForExit ();
git_config = new GitCommand (LocalPath, "config remote.origin.url \"" + RemoteUrl + "\"");
git_config.StartAndWaitForExit ();
2016-10-25 09:25:34 +00:00
git_config = new GitCommand (LocalPath, "config core.sshCommand " + GitCommand.FormatGitSSHCommand (auth_info));
2016-10-25 09:25:34 +00:00
git_config.StartAndWaitForExit();
PrepareGitLFS ();
2012-01-29 20:33:12 +00:00
}
2011-05-18 18:12:45 +00:00
2011-12-29 11:44:18 +00:00
public override List<string> ExcludePaths {
get {
List<string> rules = new List<string> ();
2012-02-08 19:42:29 +00:00
rules.Add (".git");
2011-12-29 11:44:18 +00:00
return rules;
}
}
public override double Size {
get {
2016-03-31 14:46:17 +00:00
string file_path = Path.Combine (LocalPath, ".git", "info", "size");
if (!File.Exists (file_path))
File.WriteAllText (file_path, "0");
string size = File.ReadAllText (file_path);
try {
2012-07-18 12:49:11 +00:00
return double.Parse (size);
} catch (Exception e) {
Logger.LogInfo ("Git", Name + " | Failed to parse " + file_path, e);
return 0;
}
}
}
public override double HistorySize {
get {
2016-03-31 14:46:17 +00:00
string file_path = Path.Combine (LocalPath, ".git", "info", "history_size");
if (!File.Exists (file_path))
File.WriteAllText (file_path, "0");
string size = File.ReadAllText (file_path);
try {
2012-07-18 12:49:11 +00:00
return double.Parse (size);
} catch (Exception e) {
Logger.LogInfo ("Git", Name + " | Failed to parse " + file_path, e);
return 0;
}
}
}
void UpdateSizes ()
{
2012-07-03 07:58:35 +00:00
double size = CalculateSizes (new DirectoryInfo (LocalPath));
double history_size = CalculateSizes (new DirectoryInfo (Path.Combine (LocalPath, ".git")));
2016-03-31 14:46:17 +00:00
string size_file_path = Path.Combine (LocalPath, ".git", "info", "size");
string history_size_file_path = Path.Combine (LocalPath, ".git", "info", "history_size");
File.WriteAllText (size_file_path, size.ToString ());
File.WriteAllText (history_size_file_path, history_size.ToString ());
}
2011-05-19 16:05:58 +00:00
public override string CurrentRevision {
get {
var git = new GitCommand (LocalPath, "rev-parse HEAD");
2012-07-26 10:12:14 +00:00
string output = git.StartAndReadStandardOutput ();
2011-05-19 16:05:58 +00:00
2012-07-22 09:40:49 +00:00
if (git.ExitCode == 0)
2012-07-26 10:12:14 +00:00
return output;
return null;
2011-05-19 16:05:58 +00:00
}
}
public override bool HasRemoteChanges {
get {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Checking for remote changes...");
2012-06-09 15:27:34 +00:00
string current_revision = CurrentRevision;
2012-07-26 10:12:14 +00:00
var git = new GitCommand (LocalPath,
2016-06-19 06:39:33 +00:00
"ls-remote --heads --exit-code origin " + this.branch, auth_info);
string output = git.StartAndReadStandardOutput ();
2012-07-26 10:12:14 +00:00
if (git.ExitCode != 0)
return false;
2012-07-26 10:12:14 +00:00
string remote_revision = "" + output.Substring (0, 40);
if (!remote_revision.Equals (current_revision)) {
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "merge-base " + remote_revision + " master");
git.StartAndWaitForExit ();
if (git.ExitCode != 0) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Remote changes found, local: " +
current_revision + ", remote: " + remote_revision);
Error = ErrorStatus.None;
return true;
2018-04-09 03:41:59 +00:00
} else {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Remote " + remote_revision + " is already in our history");
return false;
}
2016-05-30 03:14:40 +00:00
}
2011-08-25 15:02:34 +00:00
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | No remote changes, local+remote: " + current_revision);
return false;
2011-04-20 14:02:20 +00:00
}
}
2010-07-24 14:03:58 +00:00
2011-05-19 16:05:58 +00:00
public override bool SyncUp ()
2011-04-20 14:02:20 +00:00
{
if (!Add ()) {
Error = ErrorStatus.UnreadableFiles;
return false;
}
string message = base.status_message;
if (string.IsNullOrEmpty (message))
message = FormatCommitMessage ();
if (message != null)
Commit (message);
PrepareGitLFS ();
2016-05-30 03:14:40 +00:00
2016-06-19 06:39:33 +00:00
var git_push = new GitCommand (LocalPath, string.Format ("push --all --progress origin", RemoteUrl), auth_info);
2016-05-30 03:14:40 +00:00
git_push.StartInfo.RedirectStandardError = true;
git_push.Start ();
if (!ReadStream (git_push))
return false;
2016-05-30 03:14:40 +00:00
git_push.WaitForExit ();
2016-06-20 20:19:20 +00:00
2012-02-08 19:42:29 +00:00
UpdateSizes ();
2016-05-30 03:14:40 +00:00
if (git_push.ExitCode == 0)
return true;
2016-03-28 17:14:21 +00:00
Error = ErrorStatus.HostUnreachable;
return false;
2011-04-20 14:02:20 +00:00
}
2011-05-19 16:05:58 +00:00
public override bool SyncDown ()
2011-04-20 14:02:20 +00:00
{
2016-06-20 20:19:20 +00:00
string lfs_is_behind_file_path = Path.Combine (LocalPath, ".git", "lfs", "is_behind");
if (StorageType == StorageType.LargeFiles)
File.Create (lfs_is_behind_file_path).Close ();
2016-06-20 20:19:20 +00:00
var git_fetch = new GitCommand (LocalPath, "fetch --progress origin " + branch, auth_info);
2011-12-30 14:00:15 +00:00
git_fetch.StartInfo.RedirectStandardError = true;
git_fetch.Start ();
2011-12-30 14:00:15 +00:00
if (!ReadStream (git_fetch))
return false;
2011-12-30 14:00:15 +00:00
git_fetch.WaitForExit ();
2011-12-30 14:00:15 +00:00
if (git_fetch.ExitCode != 0) {
Error = ErrorStatus.HostUnreachable;
2011-05-19 16:05:58 +00:00
return false;
2011-04-20 14:02:20 +00:00
}
2016-06-20 20:19:20 +00:00
if (Merge ()) {
if (StorageType == StorageType.LargeFiles) {
// Pull LFS files manually to benefit from concurrency
var git_lfs_pull = new GitCommand (LocalPath, "lfs pull origin", auth_info);
git_lfs_pull.StartAndWaitForExit ();
if (git_lfs_pull.ExitCode != 0) {
Error = ErrorStatus.HostUnreachable;
return false;
}
if (File.Exists (lfs_is_behind_file_path))
File.Delete (lfs_is_behind_file_path);
}
UpdateSizes ();
return true;
}
return false;
2011-04-20 14:02:20 +00:00
}
bool ReadStream (GitCommand command)
{
StreamReader output_stream = command.StandardError;
if (StorageType == StorageType.LargeFiles)
output_stream = command.StandardOutput;
double percentage = 0;
double speed = 0;
string information = "";
while (!output_stream.EndOfStream) {
string line = output_stream.ReadLine ();
ErrorStatus error = GitCommand.ParseProgress (line, out percentage, out speed, out information);
if (error != ErrorStatus.None) {
Error = error;
information = line;
command.Kill ();
command.Dispose ();
Logger.LogInfo ("Git", Name + " | Error status changed to " + Error);
return false;
}
OnProgressChanged (percentage, speed, information);
}
return true;
}
2012-02-08 19:42:29 +00:00
public override bool HasLocalChanges {
2011-04-20 14:02:20 +00:00
get {
PrepareDirectories (LocalPath);
var git = new GitCommand (LocalPath, "status --porcelain");
2012-07-26 10:12:14 +00:00
string output = git.StartAndReadStandardOutput ();
2012-07-02 21:58:37 +00:00
return !string.IsNullOrEmpty (output);
2011-04-20 14:02:20 +00:00
}
}
2010-07-20 21:21:37 +00:00
2011-05-19 16:05:58 +00:00
public override bool HasUnsyncedChanges {
get {
2016-06-20 20:19:20 +00:00
if (StorageType == StorageType.LargeFiles) {
string lfs_is_behind_file_path = Path.Combine (LocalPath, ".git", "lfs", "is_behind");
if (File.Exists (lfs_is_behind_file_path))
return true;
}
2016-03-31 14:46:17 +00:00
string unsynced_file_path = Path.Combine (LocalPath, ".git", "has_unsynced_changes");
2011-05-19 16:05:58 +00:00
return File.Exists (unsynced_file_path);
}
2011-05-19 16:05:58 +00:00
set {
2016-03-31 14:46:17 +00:00
string unsynced_file_path = Path.Combine (LocalPath, ".git", "has_unsynced_changes");
2011-05-19 16:05:58 +00:00
2012-07-18 12:49:11 +00:00
if (value)
File.WriteAllText (unsynced_file_path, "");
else
2011-05-19 16:05:58 +00:00
File.Delete (unsynced_file_path);
}
}
2011-04-20 14:02:20 +00:00
// Stages the made changes
bool Add ()
2011-04-20 14:02:20 +00:00
{
var git = new GitCommand (LocalPath, "add --all");
2012-07-26 10:12:14 +00:00
git.StartAndWaitForExit ();
return (git.ExitCode == 0);
2011-04-20 14:02:20 +00:00
}
2010-07-22 21:10:38 +00:00
2010-07-20 21:21:37 +00:00
2011-04-20 14:02:20 +00:00
// Commits the made changes
void Commit (string message)
2012-10-20 22:22:41 +00:00
{
2018-06-15 16:44:12 +00:00
GitCommand git_config;
2012-06-24 22:20:45 +00:00
string user_name = base.local_config.User.Name;
string user_email = base.local_config.User.Email;
2012-10-20 22:22:41 +00:00
if (!this.user_is_set) {
2018-06-15 16:44:12 +00:00
git_config = new GitCommand (LocalPath, "config user.name \"" + user_name + "\"");
git_config.StartAndWaitForExit ();
2018-06-15 16:44:12 +00:00
git_config = new GitCommand (LocalPath, "config user.email \"" + user_email + "\"");
git_config.StartAndWaitForExit ();
2012-10-20 22:22:41 +00:00
this.user_is_set = true;
}
if (StorageType == StorageType.Encrypted) {
string password_file_path = Path.Combine (LocalPath, ".git", "info", "encryption_password");
string password = File.ReadAllText (password_file_path);
user_name = user_name.AESEncrypt (password);
user_email = user_email.AESEncrypt (password);
}
GitCommand git_commit;
string message_file_path = Path.Combine (LocalPath, ".git", "info", "commit_message");
try {
File.WriteAllText (message_file_path, message);
// Commit from message stored in temporary file to avoid special character conflicts on the command line
git_commit = new GitCommand (LocalPath, string.Format ("commit --all --file=\"{0}\" --author=\"{1} <{2}>\"",
message_file_path, user_name, user_email));
} catch (IOException e) {
Logger.LogInfo ("Git", Name + " | Could not create commit message file: " + message_file_path, e);
// If committing with a temporary file fails, use a simple static commit message
git_commit = new GitCommand (LocalPath, string.Format ("commit --all --message=\"{0}\" --author=\"{1} <{2}>\"",
"Changes by SparkleShare", user_name, user_email));
}
git_commit.StartAndReadStandardOutput ();
File.Delete (message_file_path);
2011-04-20 14:02:20 +00:00
}
2010-10-07 21:43:08 +00:00
2011-04-20 14:02:20 +00:00
// Merges the fetched changes
bool Merge ()
2011-04-20 14:02:20 +00:00
{
string message = FormatCommitMessage ();
2018-04-09 03:41:59 +00:00
if (message != null) {
2011-04-20 14:02:20 +00:00
Add ();
Commit (message);
2011-04-20 14:02:20 +00:00
}
2016-03-30 23:36:31 +00:00
GitCommand git;
// Stop if we're already in a merge because something went wrong
if (this.in_merge) {
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "merge --abort");
git.StartAndWaitForExit ();
2018-04-09 03:41:59 +00:00
return false;
}
// Temporarily change the ignorecase setting to true to avoid
// conflicts in file names due to letter case changes
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "config core.ignorecase true");
2012-07-26 10:12:14 +00:00
git.StartAndWaitForExit ();
2010-07-20 21:21:37 +00:00
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "merge FETCH_HEAD");
git.StartInfo.RedirectStandardOutput = false;
string error_output = git.StartAndReadStandardError ();
if (git.ExitCode != 0) {
// Stop when we can't merge due to locked local files
// error: cannot stat 'filename': Permission denied
if (error_output.Contains ("error: cannot stat")) {
Error = ErrorStatus.UnreadableFiles;
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Error status changed to " + Error);
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "merge --abort");
git.StartAndWaitForExit ();
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "config core.ignorecase false");
git.StartAndWaitForExit ();
return false;
2018-04-09 03:41:59 +00:00
} else {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", error_output);
Logger.LogInfo ("Git", Name + " | Conflict detected, trying to get out...");
2018-04-09 03:41:59 +00:00
while (this.in_merge && HasLocalChanges) {
try {
ResolveConflict ();
} catch (Exception e) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Failed to resolve conflict, trying again...", e);
}
}
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Conflict resolved");
}
}
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "config core.ignorecase false");
git.StartAndWaitForExit ();
return true;
2011-04-20 14:02:20 +00:00
}
2010-07-21 23:17:20 +00:00
void ResolveConflict ()
2011-04-28 11:49:14 +00:00
{
2012-04-29 14:19:36 +00:00
// This is a list of conflict status codes that Git uses, their
// meaning, and how SparkleShare should handle them.
//
// DD unmerged, both deleted -> Do nothing
// AU unmerged, added by us -> Use server's, save ours as a timestamped copy
// UD unmerged, deleted by them -> Use ours
// UA unmerged, added by them -> Use server's, save ours as a timestamped copy
// DU unmerged, deleted by us -> Use server's
// AA unmerged, both added -> Use server's, save ours as a timestamped copy
// UU unmerged, both modified -> Use server's, save ours as a timestamped copy
// ?? unmerged, new files -> Stage the new files
var git_status = new GitCommand (LocalPath, "status --porcelain");
2018-06-15 16:44:40 +00:00
string output = git_status.StartAndReadStandardOutput ();
2011-04-28 11:49:14 +00:00
string [] lines = output.Split ("\n".ToCharArray ());
bool trigger_conflict_event = false;
2011-04-28 11:49:14 +00:00
foreach (string line in lines) {
string conflicting_file_path = line.Substring (3);
conflicting_file_path = conflicting_file_path.Trim ("\"".ToCharArray ());
// Remove possible rename indicators
string [] separators = {" -> \"", " -> "};
foreach (string separator in separators) {
if (conflicting_file_path.Contains (separator))
conflicting_file_path = conflicting_file_path.Substring (conflicting_file_path.IndexOf (separator) + separator.Length);
}
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Conflict type: " + line);
// Ignore conflicts in hidden files and use the local versions
if (conflicting_file_path.EndsWith (".sparkleshare") || conflicting_file_path.EndsWith (".empty")) {
Logger.LogInfo ("Git", Name + " | Ignoring conflict in special file: " + conflicting_file_path);
// Recover local version
var git_ours = new GitCommand (LocalPath, "checkout --ours \"" + conflicting_file_path + "\"");
git_ours.StartAndWaitForExit ();
string abs_conflicting_path = Path.Combine (LocalPath, conflicting_file_path);
if (File.Exists (abs_conflicting_path))
File.SetAttributes (abs_conflicting_path, FileAttributes.Hidden);
continue;
}
Logger.LogInfo ("Git", Name + " | Resolving: " + conflicting_file_path);
// Both the local and server version have been modified
if (line.StartsWith ("UU") || line.StartsWith ("AA") ||
line.StartsWith ("AU") || line.StartsWith ("UA")) {
// Get the author name of the conflicting version
var git_log = new GitCommand (LocalPath, "log -n 1 FETCH_HEAD --pretty=format:%an " + conflicting_file_path);
string other_author_name = git_log.StartAndReadStandardOutput ();
// Generate distinguishing names for both versions of the file
string clue_A = string.Format (" (by {0})", base.local_config.User.Name);
string clue_B = string.Format (" (by {0})", other_author_name);
if (base.local_config.User.Name == other_author_name) {
clue_A = " (A)";
clue_B = " (B)";
}
string file_name_A = Path.GetFileNameWithoutExtension (conflicting_file_path) + clue_A + Path.GetExtension (conflicting_file_path);
string file_name_B = Path.GetFileNameWithoutExtension (conflicting_file_path) + clue_B + Path.GetExtension (conflicting_file_path);
string abs_conflicting_file_path = Path.Combine (LocalPath, conflicting_file_path);
2018-04-09 03:41:59 +00:00
string abs_file_path_A = Path.Combine (Path.GetDirectoryName (abs_conflicting_file_path), file_name_A);
string abs_file_path_B = Path.Combine (Path.GetDirectoryName (abs_conflicting_file_path), file_name_B);
2011-04-28 11:49:14 +00:00
// Recover local version
var git_checkout_A = new GitCommand (LocalPath, "checkout --ours \"" + conflicting_file_path + "\"");
git_checkout_A.StartAndWaitForExit ();
if (File.Exists (abs_conflicting_file_path) && !File.Exists (abs_file_path_A))
File.Move (abs_conflicting_file_path, abs_file_path_A);
2011-04-28 11:49:14 +00:00
// Recover server version
var git_checkout_B = new GitCommand (LocalPath, "checkout --theirs \"" + conflicting_file_path + "\"");
git_checkout_B.StartAndWaitForExit ();
if (File.Exists (abs_conflicting_file_path) && !File.Exists (abs_file_path_B))
File.Move (abs_conflicting_file_path, abs_file_path_B);
// Recover original (before both versions diverged)
var git_checkout = new GitCommand (LocalPath, "checkout ORIG_HEAD^ \"" + conflicting_file_path + "\"");
git_checkout.StartAndWaitForExit ();
2011-04-28 11:49:14 +00:00
trigger_conflict_event = true;
2012-07-02 21:10:03 +00:00
// The server version has been modified, but the local version was removed
} else if (line.StartsWith ("DU")) {
2012-11-22 12:31:48 +00:00
// The modified local version is already in the checkout, so it just needs to be added.
// We need to specifically mention the file, so we can't reuse the Add () method
var git_add = new GitCommand (LocalPath, "add \"" + conflicting_file_path + "\"");
2012-07-26 10:12:14 +00:00
git_add.StartAndWaitForExit ();
2018-04-09 03:41:59 +00:00
// The local version has been modified, but the server version was removed
} else if (line.StartsWith ("UD")) {
2018-04-09 03:41:59 +00:00
// Recover our version
var git_theirs = new GitCommand (LocalPath, "checkout --ours \"" + conflicting_file_path + "\"");
git_theirs.StartAndWaitForExit ();
2018-04-09 03:41:59 +00:00
// Server and local versions were removed
} else if (line.StartsWith ("DD")) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | No need to resolve: " + line);
// New local files
} else if (line.StartsWith ("??")) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Found new file, no need to resolve: " + line);
2018-04-09 03:41:59 +00:00
} else {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Don't know what to do with: " + line);
}
2011-04-28 11:49:14 +00:00
}
2012-07-02 21:10:03 +00:00
Add ();
var git = new GitCommand (LocalPath,
"commit --message=\"Conflict resolution\" --author=\"SparkleShare <info@sparkleshare.org>\"");
2012-07-02 21:10:03 +00:00
git.StartInfo.RedirectStandardOutput = false;
2012-07-26 10:12:14 +00:00
git.StartAndWaitForExit ();
HasUnsyncedChanges = true;
if (trigger_conflict_event)
OnConflictResolved ();
2011-04-28 11:49:14 +00:00
}
public override void RestoreFile (string path, string revision, string target_file_path)
{
if (path == null)
throw new ArgumentNullException ("path");
if (revision == null)
throw new ArgumentNullException ("revision");
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Restoring \"" + path + "\" (revision " + revision + ")");
// Restore the older file...
var git = new GitCommand (LocalPath, "checkout " + revision + " \"" + path + "\"");
git.StartAndWaitForExit ();
2012-10-14 18:57:13 +00:00
string local_file_path = Path.Combine (LocalPath, path);
2012-10-14 18:57:13 +00:00
// ...move it...
try {
File.Move (local_file_path, target_file_path);
2018-04-09 03:41:59 +00:00
} catch (Exception e) {
string message = string.Format ("Failed to move \"{0}\" to \"{1}\"", local_file_path, target_file_path);
Logger.LogInfo ("Git", Name + " | " + message, e);
}
2012-10-14 18:57:13 +00:00
// ...and restore the most recent revision
git = new GitCommand (LocalPath, "checkout " + CurrentRevision + " \"" + path + "\"");
git.StartAndWaitForExit ();
2012-10-14 18:57:13 +00:00
if (target_file_path.StartsWith (LocalPath))
new Thread (() => OnFileActivity (null)).Start ();
}
2016-03-30 23:56:48 +00:00
public override List<Change> UnsyncedChanges {
2014-10-28 21:59:57 +00:00
get {
return ParseStatus ();
}
}
2016-03-30 23:36:31 +00:00
public override List<ChangeSet> GetChangeSets ()
{
return GetChangeSetsInternal (null);
}
2016-03-30 23:36:31 +00:00
public override List<ChangeSet> GetChangeSets (string path)
{
return GetChangeSetsInternal (path);
2018-04-09 03:41:59 +00:00
}
List<ChangeSet> GetChangeSetsInternal (string path)
2011-04-20 14:02:20 +00:00
{
var change_sets = new List <ChangeSet> ();
2016-03-30 23:36:31 +00:00
GitCommand git;
2018-02-24 20:38:46 +00:00
string log_args = "--since=1.month --name-status --date=iso --find-renames --no-merges --no-color";
if (path == null) {
2018-02-24 20:38:46 +00:00
git = new GitCommand (LocalPath, "--no-pager log " + log_args);
2011-04-20 14:02:20 +00:00
} else {
path = path.Replace ("\\", "/");
2018-02-24 20:38:46 +00:00
git = new GitCommand (LocalPath, "--no-pager log " + log_args + " -- \"" + path + "\"");
}
string output = git.StartAndReadStandardOutput ();
if (path == null && string.IsNullOrWhiteSpace (output)) {
2018-02-24 20:38:46 +00:00
git = new GitCommand (LocalPath, "--no-pager log -n 75 " + log_args);
output = git.StartAndReadStandardOutput ();
}
2011-04-20 14:02:20 +00:00
2018-02-25 16:12:23 +00:00
// Offset the output so our log_regex can be simpler
string commit_sep = "commit ";
2011-04-20 14:02:20 +00:00
2018-02-25 16:12:23 +00:00
if (output.StartsWith (commit_sep))
output = output.Substring (commit_sep.Length) + "\n\n" + commit_sep;
2011-05-19 16:05:58 +00:00
2018-02-25 16:12:23 +00:00
MatchCollection matches = this.log_regex.Matches (output);
2011-04-20 14:02:20 +00:00
2018-02-25 16:12:23 +00:00
foreach (Match match in matches) {
ChangeSet change_set = ParseChangeSet (match);
2018-02-25 16:12:23 +00:00
if (change_set == null)
continue;
2018-02-25 16:12:23 +00:00
int count = 0;
foreach (string line in match.Groups ["files"].Value.Split ("\n".ToCharArray ())) {
if (count++ == 256)
break;
2018-02-25 16:12:23 +00:00
Change change = ParseChange (line);
2013-06-29 17:34:36 +00:00
2018-02-24 20:38:46 +00:00
if (change == null)
continue;
2011-05-19 16:05:58 +00:00
2018-02-24 20:38:46 +00:00
change.Timestamp = change_set.Timestamp;
2013-06-29 17:34:36 +00:00
change_set.Changes.Add (change);
}
2018-02-25 16:12:23 +00:00
if (path == null && change_sets.Count > 0) {
2016-03-30 23:36:31 +00:00
ChangeSet last_change_set = change_sets [change_sets.Count - 1];
2018-02-25 16:12:23 +00:00
// If a change set set already exists for this user and day, group into that one
if (change_set.Timestamp.Year == last_change_set.Timestamp.Year &&
2013-06-29 17:34:36 +00:00
change_set.Timestamp.Month == last_change_set.Timestamp.Month &&
2018-02-25 16:12:23 +00:00
change_set.Timestamp.Day == last_change_set.Timestamp.Day &&
2013-06-29 17:34:36 +00:00
change_set.User.Name.Equals (last_change_set.User.Name)) {
2013-06-29 17:34:36 +00:00
last_change_set.Changes.AddRange (change_set.Changes);
2013-06-29 17:34:36 +00:00
if (DateTime.Compare (last_change_set.Timestamp, change_set.Timestamp) < 1) {
last_change_set.FirstTimestamp = last_change_set.Timestamp;
2018-02-25 16:12:23 +00:00
last_change_set.Timestamp = change_set.Timestamp;
last_change_set.Revision = change_set.Revision;
} else {
2013-06-29 17:34:36 +00:00
last_change_set.FirstTimestamp = change_set.Timestamp;
}
} else {
2013-06-29 17:34:36 +00:00
change_sets.Add (change_set);
}
2018-02-25 16:12:23 +00:00
} else if (path != null) {
// Don't show removals or moves in the history list of a file
var changes = new Change [change_set.Changes.Count];
change_set.Changes.CopyTo (changes);
2018-02-25 16:12:23 +00:00
foreach (Change change in changes) {
if (!change.Path.Equals (path))
continue;
2013-06-29 17:34:36 +00:00
2018-02-25 16:12:23 +00:00
if (change.Type == ChangeType.Deleted || change.Type == ChangeType.Moved)
change_set.Changes.Remove (change);
2011-07-17 00:22:39 +00:00
}
2018-02-25 16:12:23 +00:00
change_sets.Add (change_set);
} else {
2013-06-29 17:34:36 +00:00
change_sets.Add (change_set);
2011-04-20 14:02:20 +00:00
}
}
2010-08-08 19:16:48 +00:00
return change_sets;
2011-04-20 14:02:20 +00:00
}
2010-08-29 10:38:34 +00:00
2012-07-01 08:46:42 +00:00
2018-02-25 16:12:23 +00:00
ChangeSet ParseChangeSet (Match match)
{
ChangeSet change_set = new ChangeSet ();
// Set the name and email
if (match.Groups ["name"].Value == "SparkleShare")
return null;
change_set.Folder = new SparkleFolder (Name);
change_set.Revision = match.Groups ["commit"].Value;
change_set.User = new User (match.Groups ["name"].Value, match.Groups ["email"].Value);
change_set.RemoteUrl = RemoteUrl;
if (StorageType == StorageType.Encrypted) {
string password_file_path = Path.Combine (LocalPath, ".git", "info", "encryption_password");
string password = File.ReadAllText (password_file_path);
try {
change_set.User = new User (
change_set.User.Name.AESDecrypt (password),
change_set.User.Email.AESDecrypt (password));
} catch (Exception) {
2018-02-25 16:12:23 +00:00
change_set.User = new User (match.Groups ["name"].Value, match.Groups ["email"].Value);
}
}
// Get the right date and time by offsetting the timezones
change_set.Timestamp = new DateTime (
int.Parse (match.Groups ["year"].Value), int.Parse (match.Groups ["month"].Value), int.Parse (match.Groups ["day"].Value),
int.Parse (match.Groups ["hour"].Value), int.Parse (match.Groups ["minute"].Value), int.Parse (match.Groups ["second"].Value));
string time_zone = match.Groups ["timezone"].Value;
int our_offset = TimeZone.CurrentTimeZone.GetUtcOffset (DateTime.Now).Hours;
int their_offset = int.Parse (time_zone.Substring (0, 3));
change_set.Timestamp = change_set.Timestamp.AddHours (their_offset * -1);
change_set.Timestamp = change_set.Timestamp.AddHours (our_offset);
return change_set;
}
Change ParseChange (string line)
2018-02-24 20:38:46 +00:00
{
// Skip lines containing backspace characters or the .sparkleshare file
if (line.Contains ("\\177") || line.Contains (".sparkleshare"))
return null;
// File lines start with a change type letter and then a tab character
if (!line.StartsWith ("A\t") &&
!line.StartsWith ("M\t") &&
!line.StartsWith ("D\t") &&
!line.StartsWith ("R100\t")) {
return null;
}
Change change = new Change () { Type = ChangeType.Added };
string file_path;
2018-02-25 16:12:23 +00:00
2018-02-24 20:47:42 +00:00
int first_tab_pos = line.IndexOf ('\t');
2018-02-24 20:38:46 +00:00
int last_tab_pos = line.LastIndexOf ('\t');
if (first_tab_pos == last_tab_pos) {
char type_letter = line [0];
if (type_letter == 'M')
change.Type = ChangeType.Edited;
if (type_letter == 'D')
change.Type = ChangeType.Deleted;
file_path = line.Substring (first_tab_pos + 1);
} else {
change.Type = ChangeType.Moved;
// The "to" and "from" file paths are separated by a tab
string [] parts = line.Split ("\t".ToCharArray ());
file_path = parts [1];
string to_file_path = parts [2];
to_file_path = to_file_path.Replace ("\\\"", "\"");
change.MovedToPath = to_file_path;
}
file_path = file_path.Replace ("\\\"", "\"");
change.Path = file_path;
2018-02-25 16:12:23 +00:00
string empty_name = ".empty";
2018-02-24 20:38:46 +00:00
// Handle .empty files as if they were folders
2018-02-25 16:12:23 +00:00
if (change.Path.EndsWith (empty_name)) {
change.Path = change.Path.Substring (0, change.Path.Length - empty_name.Length);
2018-02-24 20:38:46 +00:00
if (change.Type == ChangeType.Moved)
2018-02-25 16:12:23 +00:00
change.MovedToPath = change.MovedToPath.Substring (0, change.MovedToPath.Length - empty_name.Length);
2018-02-24 20:38:46 +00:00
change.IsFolder = true;
}
return change;
}
// The pre-push hook may have been changed by Git LFS, overwrite it to use our own configuration
void PrepareGitLFS ()
{
string pre_push_hook_path = Path.Combine (LocalPath, ".git", "hooks", "pre-push");
string pre_push_hook_content;
if (InstallationInfo.OperatingSystem == OS.macOS || InstallationInfo.OperatingSystem == OS.Windows) {
pre_push_hook_content =
"#!/bin/sh" + Environment.NewLine +
"env GIT_SSH_COMMAND='" + GitCommand.FormatGitSSHCommand (auth_info) + "' " +
Path.Combine (Configuration.DefaultConfiguration.BinPath, "git-lfs").Replace ("\\", "/") + " pre-push \"$@\"";
} else {
pre_push_hook_content =
"#!/bin/sh" + Environment.NewLine +
"env GIT_SSH_COMMAND='" + GitCommand.FormatGitSSHCommand (auth_info) + "' " +
"git-lfs pre-push \"$@\"";
}
if (InstallationInfo.OperatingSystem != OS.Windows) {
// TODO: Use proper API
var chmod = new Command ("chmod", "700 " + pre_push_hook_path);
chmod.StartAndWaitForExit ();
}
Directory.CreateDirectory (Path.GetDirectoryName (pre_push_hook_path));
File.WriteAllText (pre_push_hook_path, pre_push_hook_content);
}
// Git doesn't track empty directories, so this method
// fills them all with a hidden empty file.
//
// It also prevents git repositories from becoming
// git submodules by renaming the .git/HEAD file
void PrepareDirectories (string path)
{
try {
foreach (string child_path in Directory.GetDirectories (path)) {
2012-07-28 13:58:09 +00:00
if (IsSymlink (child_path))
continue;
if (child_path.EndsWith (".git")) {
if (child_path.Equals (Path.Combine (LocalPath, ".git")))
continue;
2016-06-20 20:19:20 +00:00
string HEAD_file_path = Path.Combine (child_path, "HEAD");
2018-04-09 03:41:59 +00:00
if (File.Exists (HEAD_file_path)) {
File.Move (HEAD_file_path, HEAD_file_path + ".backup");
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Renamed " + HEAD_file_path);
}
2018-04-09 03:41:59 +00:00
continue;
}
2018-04-09 03:41:59 +00:00
PrepareDirectories (child_path);
}
2018-04-09 03:41:59 +00:00
if (Directory.GetFiles (path).Length == 0 &&
Directory.GetDirectories (path).Length == 0 &&
!path.Equals (LocalPath)) {
if (!File.Exists (Path.Combine (path, ".empty"))) {
try {
File.WriteAllText (Path.Combine (path, ".empty"), "I'm a folder!");
File.SetAttributes (Path.Combine (path, ".empty"), FileAttributes.Hidden);
2012-07-18 12:49:11 +00:00
} catch (Exception e) {
Logger.LogInfo ("Git", Name + " | Failed adding empty folder " + path, e);
}
}
}
} catch (IOException e) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", "Failed preparing directory", e);
}
}
List<Change> ParseStatus ()
{
2016-03-30 23:56:48 +00:00
List<Change> changes = new List<Change> ();
var git_status = new GitCommand (LocalPath, "status --porcelain");
git_status.Start ();
2018-04-09 03:41:59 +00:00
while (!git_status.StandardOutput.EndOfStream) {
string line = git_status.StandardOutput.ReadLine ();
2018-06-15 16:44:40 +00:00
line = line.Trim ();
2018-04-09 03:41:59 +00:00
if (line.EndsWith (".empty") || line.EndsWith (".empty\""))
line = line.Replace (".empty", "");
2012-07-02 16:33:21 +00:00
2016-03-30 23:56:48 +00:00
Change change;
2018-04-09 03:41:59 +00:00
if (line.StartsWith ("R")) {
string path = line.Substring (3, line.IndexOf (" -> ") - 3).Trim ("\" ".ToCharArray ());
string moved_to_path = line.Substring (line.IndexOf (" -> ") + 4).Trim ("\" ".ToCharArray ());
2018-04-09 03:41:59 +00:00
2016-03-30 23:56:48 +00:00
change = new Change () {
Type = ChangeType.Moved,
Path = path,
MovedToPath = moved_to_path
};
2018-04-09 03:41:59 +00:00
} else {
string path = line.Substring (2).Trim ("\" ".ToCharArray ());
change = new Change () { Path = path };
2016-03-30 23:56:48 +00:00
change.Type = ChangeType.Added;
if (line.StartsWith ("M")) {
2016-03-30 23:56:48 +00:00
change.Type = ChangeType.Edited;
2018-04-09 03:41:59 +00:00
} else if (line.StartsWith ("D")) {
2016-03-30 23:56:48 +00:00
change.Type = ChangeType.Deleted;
}
}
changes.Add (change);
}
2018-04-09 03:41:59 +00:00
git_status.StandardOutput.ReadToEnd ();
git_status.WaitForExit ();
2014-10-28 21:59:57 +00:00
return changes;
}
// Creates a pretty commit message based on what has changed
string FormatCommitMessage ()
{
string message = "";
2016-03-30 23:56:48 +00:00
foreach (Change change in ParseStatus ()) {
if (change.Type == ChangeType.Moved) {
message += "< " + change.Path + "\n";
message += "> " + change.MovedToPath + "\n";
} else {
switch (change.Type) {
case ChangeType.Edited:
message += "/";
break;
case ChangeType.Deleted:
message += "-";
break;
case ChangeType.Added:
message += "+";
break;
}
2011-04-20 14:02:20 +00:00
message += " " + change.Path + "\n";
}
2011-04-20 14:02:20 +00:00
}
if (string.IsNullOrWhiteSpace (message))
return null;
else
return message;
2011-04-20 14:02:20 +00:00
}
// Recursively gets a folder's size in bytes
2016-03-31 14:46:17 +00:00
long CalculateSizes (DirectoryInfo parent)
{
long size = 0;
2012-04-10 21:19:33 +00:00
try {
foreach (DirectoryInfo directory in parent.GetDirectories ()) {
2016-04-01 08:24:01 +00:00
if (directory.FullName.IsSymlink () ||
2018-04-09 03:41:59 +00:00
directory.Name.Equals (".git") ||
directory.Name.Equals ("rebase-apply")) {
continue;
}
size += CalculateSizes (directory);
}
2012-04-10 21:19:33 +00:00
} catch (Exception e) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Local", "Error calculating directory size", e);
2012-04-10 21:19:33 +00:00
}
try {
foreach (FileInfo file in parent.GetFiles ()) {
2016-04-01 08:24:01 +00:00
if (file.FullName.IsSymlink ())
continue;
2018-04-09 03:41:59 +00:00
if (file.Name.Equals (".empty"))
File.SetAttributes (file.FullName, FileAttributes.Hidden);
else
size += file.Length;
}
2018-04-09 03:41:59 +00:00
2012-04-10 21:19:33 +00:00
} catch (Exception e) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Local", "Error calculating file size", e);
}
return size;
}
2012-07-28 13:58:09 +00:00
2018-04-09 03:41:59 +00:00
2016-03-31 14:46:17 +00:00
bool IsSymlink (string file)
2012-07-28 13:58:09 +00:00
{
FileAttributes attributes = File.GetAttributes (file);
return ((attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint);
}
2018-02-25 16:12:23 +00:00
Regex log_regex = new Regex (
"(?'commit'[a-f0-9]{40})\n" +
"Author: (?'name'.+?) <(?'email'.+?)>\n" +
"Date: (?'year'[0-9]{4})-(?'month'[0-9]{2})-(?'day'[0-9]{2}) (?'hour'[0-9]{2}):(?'minute'[0-9]{2}):(?'second'[0-9]{2}) (?'timezone'.[0-9]{4})\n" +
"\n" +
" (?'message'.+?)\n" +
"\n" +
"(?'files'.+?)\n\ncommit ", RegexOptions.Singleline | RegexOptions.Compiled);
2011-04-20 14:02:20 +00:00
}
}