SparkleShare/SparkleLib/Git/GitRepository.cs

1159 lines
44 KiB
C#
Raw Normal View History

// SparkleShare, a collaboration and sharing tool.
// Copyright (C) 2010 Hylke Bons <hylkebons@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
2013-10-11 15:13:46 +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
// 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.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
2011-03-08 23:55:21 +00:00
namespace SparkleLib.Git {
2016-03-30 23:36:31 +00:00
public class GitRepository : BaseRepository {
2011-05-18 18:12:45 +00:00
2012-10-20 22:22:41 +00:00
private bool user_is_set;
2012-10-14 18:57:13 +00:00
private bool is_encrypted;
private string cached_branch;
2012-11-18 18:49:57 +00:00
private Regex progress_regex = new Regex (@"([0-9]+)%", RegexOptions.Compiled);
private Regex speed_regex = new Regex (@"([0-9\.]+) ([KM])iB/s", RegexOptions.Compiled);
private Regex log_regex = new Regex (@"commit ([a-f0-9]{40})*\n" +
"Author: (.+) <(.+)>\n" +
"Date: ([0-9]{4})-([0-9]{2})-([0-9]{2}) " +
"([0-9]{2}):([0-9]{2}):([0-9]{2}) (.[0-9]{4})\n" +
"*", RegexOptions.Compiled);
private Regex merge_regex = new Regex (@"commit ([a-f0-9]{40})\n" +
"Merge: [a-f0-9]{7} [a-f0-9]{7}\n" +
"Author: (.+) <(.+)>\n" +
"Date: ([0-9]{4})-([0-9]{2})-([0-9]{2}) " +
"([0-9]{2}):([0-9]{2}):([0-9]{2}) (.[0-9]{4})\n" +
"*", RegexOptions.Compiled);
2012-11-18 18:49:57 +00:00
private string branch {
get {
2012-11-28 20:17:39 +00:00
if (!string.IsNullOrEmpty (this.cached_branch))
return this.cached_branch;
2016-03-30 23:36:31 +00:00
GitCommand git = new GitCommand (LocalPath, "config core.ignorecase true");
git.StartAndWaitForExit ();
while (this.in_merge && HasLocalChanges) {
2012-11-28 20:17:39 +00:00
try {
ResolveConflict ();
} 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;
}
}
private bool in_merge {
get {
string merge_file_path = new string [] { LocalPath, ".git", "MERGE_HEAD" }.Combine ();
return File.Exists (merge_file_path);
}
}
2016-03-30 23:36:31 +00:00
public GitRepository (string path, Configuration config) : base (path, config)
2012-01-29 20:33:12 +00:00
{
2016-03-30 23:36:31 +00:00
GitCommand git = new GitCommand (LocalPath, "config core.ignorecase false");
git.StartAndWaitForExit ();
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "config remote.origin.url \"" + RemoteUrl + "\"");
git.StartAndWaitForExit ();
2012-10-14 18:57:13 +00:00
string password_file_path = Path.Combine (LocalPath, ".git", "password");
if (File.Exists (password_file_path))
this.is_encrypted = true;
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 {
string file_path = new string [] { LocalPath, ".git", "info", "size" }.Combine ();
try {
2012-07-18 12:49:11 +00:00
string size = File.ReadAllText (file_path);
return double.Parse (size);
} catch {
return 0;
}
}
}
public override double HistorySize {
get {
string file_path = new string [] { LocalPath, ".git", "info", "history_size" }.Combine ();
try {
2012-07-18 12:49:11 +00:00
string size = File.ReadAllText (file_path);
return double.Parse (size);
} catch {
return 0;
}
}
}
2012-02-08 19:42:29 +00:00
private 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")));
string size_file_path = new string [] { LocalPath, ".git", "info", "size" }.Combine ();
string history_size_file_path = new string [] { LocalPath, ".git", "info", "history_size" }.Combine ();
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 {
2016-03-30 23:36:31 +00:00
GitCommand 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;
2012-07-22 09:40:49 +00:00
else
2011-05-19 16:05:58 +00:00
return null;
}
}
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
2016-03-30 23:36:31 +00:00
GitCommand git = new GitCommand (LocalPath, "ls-remote --heads --exit-code \"" + RemoteUrl + "\" " + this.branch);
2012-07-26 10:12:14 +00:00
string output = git.StartAndReadStandardOutput ();
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;
} else {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Remote " + remote_revision + " is already in our history");
return false;
}
}
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.Replace ("\"", "\\\"");
if (string.IsNullOrEmpty (message))
message = FormatCommitMessage ();
if (message != null)
Commit (message);
2016-03-30 23:36:31 +00:00
GitCommand git = new GitCommand (LocalPath, "push --progress \"" + RemoteUrl + "\" " + this.branch);
git.StartInfo.RedirectStandardError = true;
2011-05-19 16:05:58 +00:00
git.Start ();
double percentage = 1.0;
while (!git.StandardError.EndOfStream) {
string line = git.StandardError.ReadLine ();
2012-11-18 18:49:57 +00:00
Match match = this.progress_regex.Match (line);
double speed = 0.0;
double number = 0.0;
if (match.Success) {
try {
number = double.Parse (match.Groups [1].Value, new CultureInfo ("en-US"));
} catch (FormatException) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", "Error parsing progress: \"" + match.Groups [1] + "\"");
}
2011-12-30 14:00:15 +00:00
// The pushing progress consists of two stages: the "Compressing
// objects" stage which we count as 20% of the total progress, and
// the "Writing objects" stage which we count as the last 80%
if (line.StartsWith ("Compressing")) {
// "Compressing objects" stage
number = (number / 100 * 20);
} else {
// "Writing objects" stage
number = (number / 100 * 80 + 20);
2012-11-18 18:49:57 +00:00
Match speed_match = this.speed_regex.Match (line);
if (speed_match.Success) {
try {
speed = double.Parse (speed_match.Groups [1].Value, new CultureInfo ("en-US")) * 1024;
} catch (FormatException) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", "Error parsing speed: \"" + speed_match.Groups [1] + "\"");
}
if (speed_match.Groups [2].Value.Equals ("M"))
speed = speed * 1024;
}
}
} else {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | " + line);
if (FindError (line))
return false;
}
if (number >= percentage) {
percentage = number;
2012-02-09 01:46:25 +00:00
base.OnProgressChanged (percentage, speed);
}
}
2011-05-19 16:05:58 +00:00
git.WaitForExit ();
2012-02-08 19:42:29 +00:00
UpdateSizes ();
2016-03-28 17:14:21 +00:00
if (git.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-03-30 23:36:31 +00:00
GitCommand git = new GitCommand (LocalPath, "fetch --progress \"" + RemoteUrl + "\" " + this.branch);
2011-12-30 14:00:15 +00:00
git.StartInfo.RedirectStandardError = true;
2011-05-19 16:05:58 +00:00
git.Start ();
2011-12-30 14:00:15 +00:00
double percentage = 1.0;
while (!git.StandardError.EndOfStream) {
string line = git.StandardError.ReadLine ();
2012-11-18 18:49:57 +00:00
Match match = this.progress_regex.Match (line);
double speed = 0.0;
2011-12-30 14:00:15 +00:00
double number = 0.0;
if (match.Success) {
try {
number = double.Parse (match.Groups [1].Value, new CultureInfo ("en-US"));
} catch (FormatException) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", "Error parsing progress: \"" + match.Groups [1] + "\"");
}
2011-12-30 14:00:15 +00:00
// The fetching progress consists of two stages: the "Compressing
// objects" stage which we count as 20% of the total progress, and
// the "Receiving objects" stage which we count as the last 80%
if (line.StartsWith ("Compressing")) {
// "Compressing objects" stage
number = (number / 100 * 20);
} else {
// "Writing objects" stage
number = (number / 100 * 80 + 20);
2012-11-18 18:49:57 +00:00
Match speed_match = this.speed_regex.Match (line);
if (speed_match.Success) {
try {
speed = double.Parse (speed_match.Groups [1].Value, new CultureInfo ("en-US")) * 1024;
} catch (FormatException) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", "Error parsing speed: \"" + speed_match.Groups [1] + "\"");
}
if (speed_match.Groups [2].Value.Equals ("M"))
speed = speed * 1024;
2011-12-30 14:00:15 +00:00
}
}
} else {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | " + line);
if (FindError (line))
return false;
2011-12-30 14:00:15 +00:00
}
2011-12-30 14:00:15 +00:00
if (number >= percentage) {
percentage = number;
2012-02-09 01:46:25 +00:00
base.OnProgressChanged (percentage, speed);
2011-12-30 14:00:15 +00:00
}
}
2011-05-19 16:05:58 +00:00
git.WaitForExit ();
2012-02-08 19:42:29 +00:00
UpdateSizes ();
2011-12-30 14:00:15 +00:00
2011-05-22 14:46:15 +00:00
if (git.ExitCode == 0) {
2016-03-26 10:40:52 +00:00
if (Merge ())
return true;
2016-03-26 10:40:52 +00:00
else
return false;
2011-08-25 15:02:34 +00:00
2011-05-19 16:05:58 +00:00
} else {
Error = ErrorStatus.HostUnreachable;
2011-05-19 16:05:58 +00:00
return false;
2011-04-20 14:02:20 +00:00
}
}
2012-02-08 19:42:29 +00:00
public override bool HasLocalChanges {
2011-04-20 14:02:20 +00:00
get {
PrepareDirectories (LocalPath);
2016-03-30 23:36:31 +00:00
GitCommand 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 {
2012-07-28 13:58:09 +00:00
string unsynced_file_path = new string [] { LocalPath, ".git", "has_unsynced_changes" }.Combine ();
2011-05-19 16:05:58 +00:00
return File.Exists (unsynced_file_path);
}
2011-05-19 16:05:58 +00:00
set {
2012-07-28 13:58:09 +00:00
string unsynced_file_path = new string [] { LocalPath, ".git", "has_unsynced_changes" }.Combine ();
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
private bool Add ()
2011-04-20 14:02:20 +00:00
{
2016-03-30 23:36:31 +00:00
GitCommand 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
2011-05-18 22:18:11 +00:00
private void Commit (string message)
2012-10-20 22:22:41 +00:00
{
2016-03-30 23:36:31 +00:00
GitCommand git;
2012-06-24 22:20:45 +00:00
2012-10-20 22:22:41 +00:00
if (!this.user_is_set) {
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "config user.name \"" + base.local_config.User.Name + "\"");
2012-10-20 22:22:41 +00:00
git.StartAndWaitForExit ();
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "config user.email \"" + base.local_config.User.Email + "\"");
2012-10-20 22:22:41 +00:00
git.StartAndWaitForExit ();
2012-10-20 22:22:41 +00:00
this.user_is_set = true;
}
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "commit --all --message=\"" + message + "\" " +
2012-07-22 09:40:49 +00:00
"--author=\"" + base.local_config.User.Name + " <" + base.local_config.User.Email + ">\"");
2012-07-26 10:12:14 +00:00
git.StartAndReadStandardOutput ();
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
private bool Merge ()
2011-04-20 14:02:20 +00:00
{
string message = FormatCommitMessage ();
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 ();
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;
} else {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", error_output);
Logger.LogInfo ("Git", Name + " | Conflict detected, trying to get out...");
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
2011-04-28 11:49:14 +00:00
private void ResolveConflict ()
{
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
2016-03-30 23:36:31 +00:00
GitCommand git_status = new GitCommand (LocalPath, "status --porcelain");
2012-07-26 10:12:14 +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_path = line.Substring (3);
2012-07-01 08:46:42 +00:00
conflicting_path = EnsureSpecialCharacters (conflicting_path);
conflicting_path = conflicting_path.Trim ("\"".ToCharArray ());
// Remove possible rename indicators
string [] separators = {" -> \"", " -> "};
foreach (string separator in separators) {
if (conflicting_path.Contains (separator)) {
conflicting_path = conflicting_path.Substring (
conflicting_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
2012-11-22 12:31:48 +00:00
if (conflicting_path.EndsWith (".sparkleshare") || conflicting_path.EndsWith (".empty")) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Ignoring conflict in special file: " + conflicting_path);
// Recover local version
2016-03-30 23:36:31 +00:00
GitCommand git_ours = new GitCommand (LocalPath, "checkout --ours \"" + conflicting_path + "\"");
git_ours.StartAndWaitForExit ();
string abs_conflicting_path = Path.Combine (LocalPath, conflicting_path);
if (File.Exists (abs_conflicting_path))
File.SetAttributes (abs_conflicting_path, FileAttributes.Hidden);
continue;
}
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Resolving: " + conflicting_path);
// Both the local and server version have been modified
if (line.StartsWith ("UU") || line.StartsWith ("AA") ||
line.StartsWith ("AU") || line.StartsWith ("UA")) {
// Recover local version
2016-03-30 23:36:31 +00:00
GitCommand git_ours = new GitCommand (LocalPath, "checkout --ours \"" + conflicting_path + "\"");
git_ours.StartAndWaitForExit ();
2011-04-28 11:49:14 +00:00
// Append a timestamp to local version.
// Windows doesn't allow colons in the file name, so
// we use "h" between the hours and minutes instead.
string timestamp = DateTime.Now.ToString ("MMM d H\\hmm");
string our_path = Path.GetFileNameWithoutExtension (conflicting_path) +
2012-07-22 09:40:49 +00:00
" (" + base.local_config.User.Name + ", " + timestamp + ")" + Path.GetExtension (conflicting_path);
string abs_conflicting_path = Path.Combine (LocalPath, conflicting_path);
string abs_our_path = Path.Combine (LocalPath, our_path);
2011-04-28 11:49:14 +00:00
if (File.Exists (abs_conflicting_path) && !File.Exists (abs_our_path))
File.Move (abs_conflicting_path, abs_our_path);
// Recover server version
2016-03-30 23:36:31 +00:00
GitCommand git_theirs = new GitCommand (LocalPath, "checkout --theirs \"" + conflicting_path + "\"");
git_theirs.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
2016-03-30 23:36:31 +00:00
GitCommand git_add = new GitCommand (LocalPath, "add \"" + conflicting_path + "\"");
2012-07-26 10:12:14 +00:00
git_add.StartAndWaitForExit ();
// The local version has been modified, but the server version was removed
} else if (line.StartsWith ("UD")) {
// Recover server version
2016-03-30 23:36:31 +00:00
GitCommand git_theirs = new GitCommand (LocalPath, "checkout --theirs \"" + conflicting_path + "\"");
git_theirs.StartAndWaitForExit ();
// 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);
} 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 ();
2016-03-30 23:36:31 +00:00
GitCommand git = new GitCommand (LocalPath, "commit --message \"Conflict resolution by SparkleShare\"");
2012-07-02 21:10:03 +00:00
git.StartInfo.RedirectStandardOutput = false;
2012-07-26 10:12:14 +00:00
git.StartAndWaitForExit ();
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 + ")");
// git-show doesn't decrypt objects, so we can't use it to retrieve
2012-10-14 18:57:13 +00:00
// files from the index. This is a suboptimal workaround but it does the job
if (this.is_encrypted) {
// Restore the older file...
2016-03-30 23:36:31 +00:00
GitCommand git = new GitCommand (LocalPath, "checkout " + revision + " \"" + path + "\"");
2012-10-14 18:57:13 +00:00
git.StartAndWaitForExit ();
string local_file_path = Path.Combine (LocalPath, path);
// ...move it...
try {
File.Move (local_file_path, target_file_path);
} catch {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git",
2012-10-14 18:57:13 +00:00
Name + " | Could not move \"" + local_file_path + "\" to \"" + target_file_path + "\"");
}
// ...and restore the most recent revision
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "checkout " + CurrentRevision + " \"" + path + "\"");
2012-10-14 18:57:13 +00:00
git.StartAndWaitForExit ();
// The correct way
} else {
2012-10-15 20:26:27 +00:00
path = path.Replace ("\"", "\\\"");
2016-03-30 23:36:31 +00:00
GitCommand git = new GitCommand (LocalPath, "show " + revision + ":\"" + path + "\"");
2012-10-14 18:57:13 +00:00
git.Start ();
FileStream stream = File.OpenWrite (target_file_path);
git.StandardOutput.BaseStream.CopyTo (stream);
stream.Close ();
git.WaitForExit ();
}
2012-10-14 18:57:13 +00:00
if (target_file_path.StartsWith (LocalPath))
new Thread (() => OnFileActivity (null)).Start ();
}
private bool FindError (string line)
{
Error = ErrorStatus.None;
if (line.Contains ("WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!") ||
line.Contains ("WARNING: POSSIBLE DNS SPOOFING DETECTED!")) {
Error = ErrorStatus.HostIdentityChanged;
} else if (line.StartsWith ("Permission denied") ||
line.StartsWith ("ssh_exchange_identification: Connection closed by remote host") ||
line.StartsWith ("The authenticity of host")) {
Error = ErrorStatus.AuthenticationFailed;
} else if (line.EndsWith ("does not appear to be a git repository")) {
Error = ErrorStatus.NotFound;
} else if (line.EndsWith ("expected old/new/ref, got 'shallow")) {
Error = ErrorStatus.IncompatibleClientServer;
} else if (line.StartsWith ("error: Disk space exceeded") ||
line.EndsWith ("No space left on device") ||
line.EndsWith ("file write error (Disk quota exceeded)")) {
2012-11-23 10:08:50 +00:00
Error = ErrorStatus.DiskSpaceExceeded;
}
if (Error != ErrorStatus.None) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Error status changed to " + Error);
return true;
2012-10-15 22:45:03 +00:00
} else {
return false;
}
}
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);
}
2016-03-30 23:36:31 +00:00
private List<ChangeSet> GetChangeSetsInternal (string path)
2011-04-20 14:02:20 +00:00
{
2016-03-30 23:36:31 +00:00
List <ChangeSet> change_sets = new List <ChangeSet> ();
GitCommand git;
if (path == null) {
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "log --since=1.month --raw --find-renames --date=iso " +
"--format=medium --no-color --no-merges");
2011-04-20 14:02:20 +00:00
} else {
path = path.Replace ("\\", "/");
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "log --raw --find-renames --date=iso " +
"--format=medium --no-color --no-merges -- \"" + path + "\"");
}
string output = git.StartAndReadStandardOutput ();
if (path == null && string.IsNullOrWhiteSpace (output)) {
2016-03-30 23:36:31 +00:00
git = new GitCommand (LocalPath, "log -n 75 --raw --find-renames --date=iso " +
"--format=medium --no-color --no-merges");
output = git.StartAndReadStandardOutput ();
}
string [] lines = output.Split ("\n".ToCharArray ());
List<string> entries = new List <string> ();
2011-04-20 14:02:20 +00:00
2013-06-29 17:34:36 +00:00
// Split up commit entries
int line_number = 0;
bool first_pass = true;
2011-04-20 14:02:20 +00:00
string entry = "", last_entry = "";
foreach (string line in lines) {
if (line.StartsWith ("commit") && !first_pass) {
2011-04-20 14:02:20 +00:00
entries.Add (entry);
entry = "";
line_number = 0;
} else {
first_pass = false;
2011-05-19 16:05:58 +00:00
}
2013-06-29 17:34:36 +00:00
// Only parse first 250 files to prevent memory issues
if (line_number < 250) {
entry += line + "\n";
line_number++;
}
2011-05-19 16:05:58 +00:00
2011-04-20 14:02:20 +00:00
last_entry = entry;
}
2011-05-19 16:05:58 +00:00
2011-04-20 14:02:20 +00:00
entries.Add (last_entry);
2013-06-29 17:34:36 +00:00
// Parse commit entries
2011-04-20 14:02:20 +00:00
foreach (string log_entry in entries) {
2012-11-18 18:49:57 +00:00
Match match = this.log_regex.Match (log_entry);
2011-04-20 14:02:20 +00:00
if (!match.Success) {
match = this.merge_regex.Match (log_entry);
if (!match.Success)
continue;
}
2011-05-19 16:05:58 +00:00
2016-03-30 23:36:31 +00:00
ChangeSet change_set = new ChangeSet ();
2011-04-20 14:02:20 +00:00
2013-06-29 17:34:36 +00:00
change_set.Folder = new SparkleFolder (Name);
change_set.Revision = match.Groups [1].Value;
2016-03-30 23:36:31 +00:00
change_set.User = new User (match.Groups [2].Value, match.Groups [3].Value);
2013-06-29 17:34:36 +00:00
change_set.RemoteUrl = RemoteUrl;
2011-05-19 16:05:58 +00:00
2013-06-29 17:34:36 +00:00
change_set.Timestamp = new DateTime (int.Parse (match.Groups [4].Value),
int.Parse (match.Groups [5].Value), int.Parse (match.Groups [6].Value),
int.Parse (match.Groups [7].Value), int.Parse (match.Groups [8].Value),
int.Parse (match.Groups [9].Value));
2011-05-29 15:48:47 +00:00
2013-06-29 17:34:36 +00:00
string time_zone = match.Groups [10].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);
2011-05-19 16:05:58 +00:00
2013-06-29 17:34:36 +00:00
string [] entry_lines = log_entry.Split ("\n".ToCharArray ());
2013-06-29 17:34:36 +00:00
// Parse file list. Lines containing file changes start with ":"
foreach (string entry_line in entry_lines) {
// Skip lines containing backspace characters
if (!entry_line.StartsWith (":") || entry_line.Contains ("\\177"))
continue;
2011-09-14 15:57:40 +00:00
2013-06-29 17:34:36 +00:00
string file_path = entry_line.Substring (39);
2013-06-29 17:34:36 +00:00
if (file_path.Equals (".sparkleshare"))
continue;
2013-06-29 17:34:36 +00:00
string type_letter = entry_line [37].ToString ();
bool change_is_folder = false;
if (file_path.EndsWith (".empty")) {
file_path = file_path.Substring (0, file_path.Length - ".empty".Length);
change_is_folder = true;
}
try {
file_path = EnsureSpecialCharacters (file_path);
} catch (Exception e) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Local", "Error parsing file name '" + file_path + "'", e);
continue;
}
2013-06-29 17:34:36 +00:00
file_path = file_path.Replace ("\\\"", "\"");
2016-03-30 23:56:48 +00:00
Change change = new Change () {
2013-06-29 17:34:36 +00:00
Path = file_path,
IsFolder = change_is_folder,
Timestamp = change_set.Timestamp,
2016-03-30 23:56:48 +00:00
Type = ChangeType.Added
2013-06-29 17:34:36 +00:00
};
if (type_letter.Equals ("R")) {
int tab_pos = entry_line.LastIndexOf ("\t");
file_path = entry_line.Substring (42, tab_pos - 42);
string to_file_path = entry_line.Substring (tab_pos + 1);
try {
file_path = EnsureSpecialCharacters (file_path);
} catch (Exception e) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Local", "Error parsing file name '" + file_path + "'", e);
continue;
}
try {
to_file_path = EnsureSpecialCharacters (to_file_path);
} catch (Exception e) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Local", "Error parsing file name '" + to_file_path + "'", e);
continue;
}
2013-06-29 17:34:36 +00:00
file_path = file_path.Replace ("\\\"", "\"");
to_file_path = to_file_path.Replace ("\\\"", "\"");
if (file_path.EndsWith (".empty")) {
file_path = file_path.Substring (0, file_path.Length - 6);
change_is_folder = true;
2011-04-20 14:02:20 +00:00
}
2013-06-29 17:34:36 +00:00
if (to_file_path.EndsWith (".empty")) {
to_file_path = to_file_path.Substring (0, to_file_path.Length - 6);
change_is_folder = true;
}
change.Path = file_path;
change.MovedToPath = to_file_path;
2016-03-30 23:56:48 +00:00
change.Type = ChangeType.Moved;
2013-06-29 17:34:36 +00:00
} else if (type_letter.Equals ("M")) {
2016-03-30 23:56:48 +00:00
change.Type = ChangeType.Edited;
2013-06-29 17:34:36 +00:00
} else if (type_letter.Equals ("D")) {
2016-03-30 23:56:48 +00:00
change.Type = ChangeType.Deleted;
2011-04-20 14:02:20 +00:00
}
2011-05-19 16:05:58 +00:00
2013-06-29 17:34:36 +00:00
change_set.Changes.Add (change);
}
2013-06-29 17:34:36 +00:00
// Group commits per user, per day
if (change_sets.Count > 0 && path == null) {
2016-03-30 23:36:31 +00:00
ChangeSet last_change_set = change_sets [change_sets.Count - 1];
2013-06-29 17:34:36 +00:00
if (change_set.Timestamp.Year == last_change_set.Timestamp.Year &&
change_set.Timestamp.Month == last_change_set.Timestamp.Month &&
change_set.Timestamp.Day == last_change_set.Timestamp.Day &&
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;
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);
}
2013-06-29 17:34:36 +00:00
} else {
// Don't show removals or moves in the revision list of a file
if (path != null) {
2016-03-30 23:56:48 +00:00
List<Change> changes_to_skip = new List<Change> ();
2016-03-30 23:56:48 +00:00
foreach (Change change in change_set.Changes) {
if ((change.Type == ChangeType.Deleted || change.Type == ChangeType.Moved)
2013-06-29 17:34:36 +00:00
&& change.Path.Equals (path)) {
2013-06-29 17:34:36 +00:00
changes_to_skip.Add (change);
}
}
2013-06-29 17:34:36 +00:00
2016-03-30 23:56:48 +00:00
foreach (Change change_to_skip in changes_to_skip)
2013-06-29 17:34:36 +00:00
change_set.Changes.Remove (change_to_skip);
2011-07-17 00:22:39 +00:00
}
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
private string EnsureSpecialCharacters (string path)
{
2012-07-01 08:46:42 +00:00
// The path is quoted if it contains special characters
if (path.StartsWith ("\""))
path = ResolveSpecialChars (path.Substring (1, path.Length - 2));
return path;
}
2012-07-01 08:46:42 +00:00
private string ResolveSpecialChars (string s)
{
2012-07-01 08:46:42 +00:00
StringBuilder builder = new StringBuilder (s.Length);
List<byte> codes = new List<byte> ();
for (int i = 0; i < s.Length; i++) {
while (s [i] == '\\' &&
s.Length - i > 3 &&
char.IsNumber (s [i + 1]) &&
char.IsNumber (s [i + 2]) &&
char.IsNumber (s [i + 3])) {
codes.Add (Convert.ToByte (s.Substring (i + 1, 3), 8));
i += 4;
}
2012-07-01 08:46:42 +00:00
if (codes.Count > 0) {
builder.Append (Encoding.UTF8.GetString (codes.ToArray ()));
codes.Clear ();
}
2012-07-01 08:46:42 +00:00
builder.Append (s [i]);
}
2012-07-01 08:46:42 +00:00
return builder.ToString ();
}
// 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
private 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;
string HEAD_file_path = Path.Combine (child_path, "HEAD");
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);
}
continue;
}
PrepareDirectories (child_path);
}
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 {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", Name + " | Failed adding empty folder " + path);
}
}
}
} catch (IOException e) {
2016-03-30 23:36:31 +00:00
Logger.LogInfo ("Git", "Failed preparing directory", e);
}
}
2016-03-30 23:56:48 +00:00
private List<Change> ParseStatus ()
{
2016-03-30 23:56:48 +00:00
List<Change> changes = new List<Change> ();
2016-03-30 23:36:31 +00:00
GitCommand git_status = new GitCommand (LocalPath, "status --porcelain");
git_status.Start ();
while (!git_status.StandardOutput.EndOfStream) {
string line = git_status.StandardOutput.ReadLine ();
line = line.Trim ();
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;
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 ());
2016-03-30 23:56:48 +00:00
change = new Change () {
Type = ChangeType.Moved,
Path = EnsureSpecialCharacters (path),
MovedToPath = EnsureSpecialCharacters (moved_to_path)
};
} else {
string path = line.Substring (2).Trim ("\" ".ToCharArray ());
2016-03-30 23:56:48 +00:00
change = new Change () { Path = EnsureSpecialCharacters (path) };
change.Type = ChangeType.Added;
if (line.StartsWith ("M")) {
2016-03-30 23:56:48 +00:00
change.Type = ChangeType.Edited;
} else if (line.StartsWith ("D")) {
2016-03-30 23:56:48 +00:00
change.Type = ChangeType.Deleted;
}
}
changes.Add (change);
}
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
private string FormatCommitMessage ()
{
string message = "";
2016-03-30 23:56:48 +00:00
foreach (Change change in ParseStatus ()) {
if (change.Type == ChangeType.Moved) {
message += "< " + EnsureSpecialCharacters (change.Path) + "\n";
message += "> " + EnsureSpecialCharacters (change.MovedToPath) + "\n";
} else {
2016-03-30 23:56:48 +00:00
if (change.Type == ChangeType.Edited) {
message += "/";
2016-03-30 23:56:48 +00:00
} else if (change.Type == ChangeType.Deleted) {
message += "-";
2011-04-20 14:02:20 +00:00
2016-03-30 23:56:48 +00:00
} else if (change.Type == ChangeType.Added) {
message += "+";
}
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
private long CalculateSizes (DirectoryInfo parent)
{
long size = 0;
2012-04-10 21:19:33 +00:00
try {
foreach (DirectoryInfo directory in parent.GetDirectories ()) {
if (directory.IsSymlink () ||
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 ()) {
if (file.IsSymlink ())
continue;
if (file.Name.Equals (".empty"))
File.SetAttributes (file.FullName, FileAttributes.Hidden);
else
size += file.Length;
}
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
private bool IsSymlink (string file)
{
FileAttributes attributes = File.GetAttributes (file);
return ((attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint);
}
2011-04-20 14:02:20 +00:00
}
}