SparkleShare/SparkleLib/Git/SparkleRepoGit.cs

1023 lines
37 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
// it under the terms of the GNU 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
2010-07-22 21:10:38 +00:00
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2011-02-23 01:13:54 +00:00
using System;
using System.Collections.Generic;
using System.IO;
2012-07-01 08:46:42 +00:00
using System.Text;
using System.Text.RegularExpressions;
using SparkleLib;
2011-03-08 23:55:21 +00:00
namespace SparkleLib.Git {
public class SparkleRepo : SparkleRepoBase {
2011-05-18 18:12:45 +00:00
private bool user_is_set;
private bool remote_url_is_set;
private bool use_git_bin;
public SparkleRepo (string path) : base (path)
2012-01-29 20:33:12 +00:00
{
SparkleGit git = new SparkleGit (LocalPath, "config --get filter.bin.clean");
git.Start ();
git.WaitForExit ();
this.use_git_bin = (git.ExitCode == 0);
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 {
2012-01-24 13:46:57 +00:00
string file_path = new string [] {LocalPath, ".git", "repo_size"}.Combine ();
try {
return double.Parse (File.ReadAllText (file_path));
} catch {
return 0;
}
}
}
public override double HistorySize {
get {
2012-01-24 13:46:57 +00:00
string file_path = new string [] {LocalPath, ".git", "repo_history_size"}.Combine ();
try {
return double.Parse (File.ReadAllText (file_path));
} catch {
return 0;
}
}
}
2012-02-08 19:42:29 +00:00
private void UpdateSizes ()
{
2012-02-08 19:42:29 +00:00
double size = CalculateSizes (
new DirectoryInfo (LocalPath));
2012-02-08 19:42:29 +00:00
double history_size = CalculateSizes (
new DirectoryInfo (Path.Combine (LocalPath, ".git")));
2012-01-24 13:46:57 +00:00
string size_file_path = new string [] {LocalPath, ".git", "repo_size"}.Combine ();
string history_size_file_path = new string [] {LocalPath, ".git", "repo_history_size"}.Combine ();
File.WriteAllText (size_file_path, size.ToString ());
File.WriteAllText (history_size_file_path, history_size.ToString ());
}
public override void CreateInitialChangeSet ()
{
base.CreateInitialChangeSet ();
SyncUp (); // FIXME: Weird freeze happens when base class handles this
}
public override string [] UnsyncedFilePaths {
get {
List<string> file_paths = new List<string> ();
SparkleGit git = new SparkleGit (LocalPath, "status --porcelain");
git.Start ();
// Reading the standard output HAS to go before
// WaitForExit, or it will hang forever on output > 4096 bytes
string output = git.StandardOutput.ReadToEnd ().TrimEnd ();
git.WaitForExit ();
string [] lines = output.Split ("\n".ToCharArray ());
foreach (string line in lines) {
if (line [1].ToString ().Equals ("M") ||
line [1].ToString ().Equals ("?") ||
line [1].ToString ().Equals ("A")) {
string path = line.Substring (3);
path = path.Trim ("\"".ToCharArray ());
file_paths.Add (path);
}
}
return file_paths.ToArray ();
}
}
2011-05-19 16:05:58 +00:00
public override string CurrentRevision {
get {
// Remove stale rebase-apply files because it
// makes the method return the wrong hashes.
string rebase_apply_file = SparkleHelpers.CombineMore (LocalPath, ".git", "rebase-apply");
2011-05-19 16:05:58 +00:00
if (File.Exists (rebase_apply_file))
File.Delete (rebase_apply_file);
SparkleGit git = new SparkleGit (LocalPath, "rev-parse HEAD");
2011-05-19 16:05:58 +00:00
git.Start ();
string output = git.StandardOutput.ReadToEnd ();
2011-05-19 16:05:58 +00:00
git.WaitForExit ();
if (git.ExitCode == 0) {
return output.TrimEnd ();
2011-08-25 15:02:34 +00:00
2011-05-19 16:05:58 +00:00
} else {
return null;
}
}
}
public override bool HasRemoteChanges {
get {
SparkleHelpers.DebugInfo ("Git", Name + " | Checking for remote changes...");
2012-06-09 15:27:34 +00:00
string current_revision = CurrentRevision;
SparkleGit git = new SparkleGit (LocalPath, "ls-remote --heads --exit-code \"" + RemoteUrl + "\" master");
git.Start ();
git.WaitForExit ();
if (git.ExitCode != 0)
return false;
2012-06-17 20:56:27 +00:00
string remote_revision = git.StandardOutput.ReadToEnd ().Substring (0, 40);
2012-06-09 15:27:34 +00:00
if (!remote_revision.StartsWith (current_revision)) {
SparkleHelpers.DebugInfo ("Git",
Name + " | Remote changes detected (local: " +
2012-06-09 15:27:34 +00:00
current_revision + ", remote: " + remote_revision + ")");
return true;
2011-08-25 15:02:34 +00:00
} else {
2012-06-09 15:27:34 +00:00
SparkleHelpers.DebugInfo ("Git",
Name + " | No remote changes detected (local+remote: " + current_revision + ")");
2012-06-09 15:27:34 +00:00
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
{
2012-02-08 19:42:29 +00:00
if (HasLocalChanges) {
Add ();
string message = FormatCommitMessage ();
Commit (message);
}
2011-04-20 14:02:20 +00:00
SparkleGit git;
if (this.use_git_bin) {
if (this.remote_url_is_set) {
git = new SparkleGit (LocalPath, "config remote.origin.url \"" + RemoteUrl + "\"");
git.Start ();
git.WaitForExit ();
this.remote_url_is_set = true;
}
SparkleGitBin git_bin = new SparkleGitBin (LocalPath, "push");
git_bin.Start ();
git_bin.WaitForExit ();
// TODO: Progress
}
git = new SparkleGit (LocalPath,
"push --progress " + // Redirects progress stats to standarderror
2012-04-29 14:19:36 +00:00
"\"" + RemoteUrl + "\" master");
git.StartInfo.RedirectStandardError = true;
2011-05-19 16:05:58 +00:00
git.Start ();
double percentage = 1.0;
Regex progress_regex = new Regex (@"([0-9]+)%", RegexOptions.Compiled);
while (!git.StandardError.EndOfStream) {
string line = git.StandardError.ReadLine ();
Match match = progress_regex.Match (line);
string speed = "";
double number = 0.0;
if (match.Success) {
number = double.Parse (match.Groups [1].Value);
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 {
if (line.StartsWith ("ERROR: QUOTA EXCEEDED")) {
int quota_limit = int.Parse (line.Substring (21).Trim ());
throw new QuotaExceededException ("Quota exceeded", quota_limit);
}
// "Writing objects" stage
number = (number / 100 * 80 + 20);
if (line.Contains ("|")) {
speed = line.Substring (line.IndexOf ("|") + 1).Trim ();
speed = speed.Replace (", done.", "").Trim ();
speed = speed.Replace ("i", "");
speed = speed.Replace ("KB/s", "ᴋʙ/s");
speed = speed.Replace ("MB/s", "ᴍʙ/s");
}
}
} else {
SparkleHelpers.DebugInfo ("Git", Name + " | " + line);
}
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 ();
2011-04-20 14:02:20 +00:00
2012-02-08 19:42:29 +00:00
UpdateSizes ();
2012-04-29 14:19:36 +00:00
ChangeSets = GetChangeSets ();
if (git.ExitCode == 0) {
ClearCache ();
return true;
} else {
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
{
2012-04-29 14:19:36 +00:00
SparkleGit git = new SparkleGit (LocalPath, "fetch --progress \"" + RemoteUrl + "\" master");
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;
Regex progress_regex = new Regex (@"([0-9]+)%", RegexOptions.Compiled);
while (!git.StandardError.EndOfStream) {
string line = git.StandardError.ReadLine ();
Match match = progress_regex.Match (line);
string speed = "";
double number = 0.0;
if (match.Success) {
number = double.Parse (match.Groups [1].Value);
// 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);
if (line.Contains ("|")) {
speed = line.Substring (line.IndexOf ("|") + 1).Trim ();
speed = speed.Replace (", done.", "").Trim ();
speed = speed.Replace ("i", "");
speed = speed.Replace ("KB/s", "ᴋʙ/s");
speed = speed.Replace ("MB/s", "ᴍʙ/s");
}
}
} else {
SparkleHelpers.DebugInfo ("Git", Name + " | " + line);
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 ();
2010-07-20 21:21:37 +00:00
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) {
2011-05-19 16:05:58 +00:00
Rebase ();
File.SetAttributes (
Path.Combine (LocalPath, ".sparkleshare"),
FileAttributes.Hidden
);
2012-04-29 14:19:36 +00:00
ChangeSets = GetChangeSets ();
ClearCache ();
2012-04-29 14:19:36 +00:00
return true;
2011-08-25 15:02:34 +00:00
2011-05-19 16:05:58 +00:00
} else {
2012-04-29 14:19:36 +00:00
ChangeSets = GetChangeSets ();
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);
2011-04-20 14:02:20 +00:00
SparkleGit git = new SparkleGit (LocalPath, "status --porcelain");
git.Start ();
// Reading the standard output HAS to go before
// WaitForExit, or it will hang forever on output > 4096 bytes
string output = git.StandardOutput.ReadToEnd ().TrimEnd ();
git.WaitForExit ();
string [] lines = output.Split ("\n".ToCharArray ());
foreach (string line in lines) {
if (line.Trim ().Length > 0)
return true;
}
return false;
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 {
2011-05-19 16:05:58 +00:00
string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath,
".git", "has_unsynced_changes");
return File.Exists (unsynced_file_path);
}
2011-05-19 16:05:58 +00:00
set {
string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath,
".git", "has_unsynced_changes");
if (value) {
if (!File.Exists (unsynced_file_path))
File.Create (unsynced_file_path).Close ();
2011-05-19 16:05:58 +00:00
} else {
File.Delete (unsynced_file_path);
}
}
}
2011-04-20 14:02:20 +00:00
// Stages the made changes
private void Add ()
{
SparkleGit git = new SparkleGit (LocalPath, "add --all");
git.Start ();
git.WaitForExit ();
SparkleHelpers.DebugInfo ("Git", Name + " | Changes staged");
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-06-24 22:20:45 +00:00
SparkleGit git;
if (!this.user_is_set) {
2012-06-24 22:20:45 +00:00
git = new SparkleGit (LocalPath,
"config user.name \"" + SparkleConfig.DefaultConfig.User.Name + "\"");
2012-06-24 22:20:45 +00:00
git.Start ();
git.WaitForExit ();
2012-06-24 22:20:45 +00:00
git = new SparkleGit (LocalPath,
"config user.email \"" + SparkleConfig.DefaultConfig.User.Email + "\"");
2012-06-24 22:20:45 +00:00
git.Start ();
git.WaitForExit ();
this.user_is_set = true;
}
2012-06-24 22:20:45 +00:00
git = new SparkleGit (LocalPath,
"commit -m \"" + message + "\" " +
"--author=\"" + SparkleConfig.DefaultConfig.User.Name +
" <" + SparkleConfig.DefaultConfig.User.Email + ">\"");
2011-04-20 14:02:20 +00:00
git.Start ();
git.StandardOutput.ReadToEnd ();
2011-04-20 14:02:20 +00:00
git.WaitForExit ();
}
2010-10-07 21:43:08 +00:00
2011-04-20 14:02:20 +00:00
// Merges the fetched changes
2011-05-18 22:18:11 +00:00
private void Rebase ()
2011-04-20 14:02:20 +00:00
{
2012-02-08 19:42:29 +00:00
if (HasLocalChanges) {
2011-04-20 14:02:20 +00:00
Add ();
2011-05-19 16:05:58 +00:00
2011-04-20 14:02:20 +00:00
string commit_message = FormatCommitMessage ();
Commit (commit_message);
}
SparkleGit git = new SparkleGit (LocalPath, "rebase FETCH_HEAD");
git.StartInfo.RedirectStandardOutput = false;
2011-04-20 14:02:20 +00:00
git.Start ();
git.WaitForExit ();
2010-07-20 21:21:37 +00:00
if (git.ExitCode != 0) {
SparkleHelpers.DebugInfo ("Git", Name + " | Conflict detected, trying to get out...");
2012-02-08 19:42:29 +00:00
while (HasLocalChanges)
ResolveConflict ();
SparkleHelpers.DebugInfo ("Git", Name + " | Conflict resolved");
2011-05-19 16:05:58 +00:00
OnConflictResolved ();
}
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 theirs, save ours as a timestamped copy
// UD unmerged, deleted by them -> Use ours
// UA unmerged, added by them -> Use theirs, save ours as a timestamped copy
// DU unmerged, deleted by us -> Use theirs
// AA unmerged, both added -> Use theirs, save ours as a timestamped copy
// UU unmerged, both modified -> Use theirs, save ours as a timestamped copy
// ?? unmerged, new files -> Stage the new files
//
// Note that a rebase merge works by replaying each commit from the working branch on
// top of the upstream branch. Because of this, when a merge conflict happens the
// side reported as 'ours' is the so-far rebased series, starting with upstream,
// and 'theirs' is the working branch. In other words, the sides are swapped.
//
// So: 'ours' means the 'server's version' and 'theirs' means the 'local version'
2011-04-28 11:49:14 +00:00
SparkleGit git_status = new SparkleGit (LocalPath, "status --porcelain");
git_status.Start ();
// Reading the standard output HAS to go before
// WaitForExit, or it will hang forever on output > 4096 bytes
string output = git_status.StandardOutput.ReadToEnd ().TrimEnd ();
2011-06-29 21:40:34 +00:00
git_status.WaitForExit ();
2011-04-28 11:49:14 +00:00
string [] lines = output.Split ("\n".ToCharArray ());
foreach (string line in lines) {
string conflicting_path = line.Substring (3);
2012-07-01 08:46:42 +00:00
conflicting_path = EnsureSpecialCharacters (conflicting_path);
SparkleHelpers.DebugInfo ("Git", Name + " | Conflict type: " + line);
// Ignore conflicts in the .sparkleshare file and use the local version
if (conflicting_path.EndsWith (".sparkleshare") ||
conflicting_path.EndsWith (".empty")) {
// Recover local version
SparkleGit git_theirs = new SparkleGit (LocalPath,
"checkout --theirs \"" + conflicting_path + "\"");
git_theirs.Start ();
git_theirs.WaitForExit ();
File.SetAttributes (Path.Combine (LocalPath, conflicting_path), FileAttributes.Hidden);
SparkleGit git_rebase_continue = new SparkleGit (LocalPath, "rebase --continue");
git_rebase_continue.Start ();
git_rebase_continue.WaitForExit ();
continue;
}
// 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
SparkleGit git_theirs = new SparkleGit (LocalPath,
"checkout --theirs \"" + conflicting_path + "\"");
git_theirs.Start ();
git_theirs.WaitForExit ();
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 their_path = Path.GetFileNameWithoutExtension (conflicting_path) +
" (" + SparkleConfig.DefaultConfig.User.Name + ", " + timestamp + ")" +
Path.GetExtension (conflicting_path);
string abs_conflicting_path = Path.Combine (LocalPath, conflicting_path);
string abs_their_path = Path.Combine (LocalPath, their_path);
2011-04-28 11:49:14 +00:00
File.Move (abs_conflicting_path, abs_their_path);
// Recover server version
2011-04-28 11:49:14 +00:00
SparkleGit git_ours = new SparkleGit (LocalPath,
"checkout --ours \"" + conflicting_path + "\"");
2011-04-28 11:49:14 +00:00
git_ours.Start ();
git_ours.WaitForExit ();
Add ();
2011-04-28 11:49:14 +00:00
SparkleGit git_rebase_continue = new SparkleGit (LocalPath, "rebase --continue");
git_rebase_continue.Start ();
git_rebase_continue.WaitForExit ();
// The local version has been modified, but the server version was removed
} else if (line.StartsWith ("DU")) {
// 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
SparkleGit git_add = new SparkleGit (LocalPath,
"add \"" + conflicting_path + "\"");
git_add.Start ();
git_add.WaitForExit ();
SparkleGit git_rebase_continue = new SparkleGit (LocalPath, "rebase --continue");
git_rebase_continue.Start ();
git_rebase_continue.WaitForExit ();
// The server version has been modified, but the local version was removed
} else if (line.StartsWith ("UD")) {
// We can just skip here, the server version is
// already in the checkout
SparkleGit git_rebase_skip = new SparkleGit (LocalPath, "rebase --skip");
git_rebase_skip.Start ();
git_rebase_skip.WaitForExit ();
// New local files
} else {
Add ();
SparkleGit git_rebase_continue = new SparkleGit (LocalPath, "rebase --continue");
git_rebase_continue.Start ();
git_rebase_continue.WaitForExit ();
}
2011-04-28 11:49:14 +00:00
}
}
2011-05-18 18:12:45 +00:00
// Returns a list of the latest change sets
2012-04-29 14:19:36 +00:00
public override List<SparkleChangeSet> GetChangeSets (int count)
2011-04-20 14:02:20 +00:00
{
2011-07-23 20:34:04 +00:00
if (count < 1)
count = 30;
2011-05-19 16:05:58 +00:00
count = 150;
List <SparkleChangeSet> change_sets = new List <SparkleChangeSet> ();
2011-04-20 14:02:20 +00:00
SparkleGit git_log = new SparkleGit (LocalPath,
"log -" + count + " --raw -M --date=iso --format=medium --no-color --no-merges");
2011-04-20 14:02:20 +00:00
git_log.Start ();
2011-05-19 16:05:58 +00:00
2011-04-20 14:02:20 +00:00
// Reading the standard output HAS to go before
// WaitForExit, or it will hang forever on output > 4096 bytes
string output = git_log.StandardOutput.ReadToEnd ();
git_log.WaitForExit ();
string [] lines = output.Split ("\n".ToCharArray ());
List <string> entries = new List <string> ();
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
}
// Only parse 250 files to prevent memory issues
if (line_number < 254) {
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);
Regex regex = new Regex (@"commit ([a-z0-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);
2011-04-20 14:02:20 +00:00
foreach (string log_entry in entries) {
Match match = regex.Match (log_entry);
if (match.Success) {
SparkleChangeSet change_set = new SparkleChangeSet ();
2011-05-19 16:05:58 +00:00
change_set.Folder = new SparkleFolder (Name);
change_set.Revision = match.Groups [1].Value;
change_set.User = new SparkleUser (match.Groups [2].Value, match.Groups [3].Value);
change_set.RemoteUrl = RemoteUrl;
2011-04-20 14:02:20 +00:00
change_set.Timestamp = new DateTime (int.Parse (match.Groups [4].Value),
2011-04-20 14:02:20 +00:00
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-19 16:05:58 +00:00
2011-06-14 23:34:29 +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-29 15:48:47 +00:00
2011-04-20 14:02:20 +00:00
string [] entry_lines = log_entry.Split ("\n".ToCharArray ());
2011-05-19 16:05:58 +00:00
2011-04-20 14:02:20 +00:00
foreach (string entry_line in entry_lines) {
if (entry_line.StartsWith (":")) {
2011-05-19 16:05:58 +00:00
2011-04-20 14:02:20 +00:00
string change_type = entry_line [37].ToString ();
string file_path = entry_line.Substring (39);
// Handle filepath with special characters
file_path = EnsureSpecialCharacters (file_path);
string to_file_path;
2011-05-19 16:05:58 +00:00
2011-09-14 15:57:40 +00:00
if (file_path.EndsWith (".empty"))
file_path = file_path.Substring (0,
file_path.Length - ".empty".Length);
2011-09-14 15:57:40 +00:00
if (file_path.Equals (".sparkleshare"))
continue;
if (change_type.Equals ("A")) {
change_set.Changes.Add (
new SparkleChange () {
Path = file_path,
Timestamp = change_set.Timestamp,
Type = SparkleChangeType.Added
}
);
2011-06-09 23:01:45 +00:00
2011-04-20 14:02:20 +00:00
} else if (change_type.Equals ("M")) {
change_set.Changes.Add (
new SparkleChange () {
Path = file_path,
Timestamp = change_set.Timestamp,
Type = SparkleChangeType.Edited
}
);
2011-06-09 23:01:45 +00:00
2011-04-20 14:02:20 +00:00
} else if (change_type.Equals ("D")) {
change_set.Changes.Add (
new SparkleChange () {
Path = file_path,
Timestamp = change_set.Timestamp,
Type = SparkleChangeType.Deleted
}
);
2011-06-09 23:01:45 +00:00
} else if (change_type.Equals ("R")) {
int tab_pos = entry_line.LastIndexOf ("\t");
file_path = entry_line.Substring (42, tab_pos - 42);
to_file_path = entry_line.Substring (tab_pos + 1);
2012-07-01 08:46:42 +00:00
file_path = EnsureSpecialCharacters (file_path);
to_file_path = EnsureSpecialCharacters (to_file_path);
if (file_path.EndsWith (".empty"))
file_path = file_path.Substring (0, file_path.Length - 6);
if (to_file_path.EndsWith (".empty"))
to_file_path = to_file_path.Substring (0, to_file_path.Length - 6);
change_set.Changes.Add (
new SparkleChange () {
Path = file_path,
MovedToPath = to_file_path,
Timestamp = change_set.Timestamp,
Type = SparkleChangeType.Moved
}
);
2011-04-20 14:02:20 +00:00
}
}
}
2011-05-19 16:05:58 +00:00
if (change_set.Changes.Count > 0) {
if (change_sets.Count > 0) {
SparkleChangeSet last_change_set = change_sets [change_sets.Count - 1];
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)) {
last_change_set.Changes.AddRange (change_set.Changes);
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;
2011-07-17 00:22:39 +00:00
} else {
last_change_set.FirstTimestamp = change_set.Timestamp;
}
} else {
change_sets.Add (change_set);
}
} else {
change_sets.Add (change_set);
}
2011-07-17 00:22:39 +00:00
}
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
/// <summary>
/// Resolves special characters like \303\244 (ä) to their real character
/// </summary>
/// <param name="file_path"></param>
/// <returns></returns>
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 ();
}
private void ClearCache ()
{
if (!this.use_git_bin)
return;
SparkleGitBin git_bin = new SparkleGitBin (LocalPath, "clear -f");
git_bin.Start ();
git_bin.WaitForExit ();
}
// 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)) {
if (SparkleHelpers.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");
SparkleHelpers.DebugInfo ("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);
} catch {
SparkleHelpers.DebugInfo ("Git", Name + " | Failed adding empty folder " + path);
}
}
}
} catch (IOException e) {
SparkleHelpers.DebugInfo ("Git", "Failed preparing directory: " + e.Message);
}
}
2011-04-20 14:02:20 +00:00
// Creates a pretty commit message based on what has changed
private string FormatCommitMessage ()
{
List<string> Added = new List<string> ();
List<string> Modified = new List<string> ();
List<string> Removed = new List<string> ();
string file_name = "";
string message = "";
SparkleGit git_status = new SparkleGit (LocalPath, "status --porcelain");
git_status.Start ();
// Reading the standard output HAS to go before
// WaitForExit, or it will hang forever on output > 4096 bytes
string output = git_status.StandardOutput.ReadToEnd ().Trim ("\n".ToCharArray ());
git_status.WaitForExit ();
string [] lines = output.Split ("\n".ToCharArray ());
foreach (string line in lines) {
if (line.StartsWith ("A"))
Added.Add (line.Substring (3));
else if (line.StartsWith ("M"))
Modified.Add (line.Substring (3));
else if (line.StartsWith ("D"))
Removed.Add (line.Substring (3));
else if (line.StartsWith ("R")) {
Removed.Add (line.Substring (3, (line.IndexOf (" -> ") - 3)));
Added.Add (line.Substring (line.IndexOf (" -> ") + 4));
}
}
int count = 0;
int max_count = 20;
string n = Environment.NewLine;
foreach (string added in Added) {
file_name = added.Trim ("\"".ToCharArray ());
if (file_name.EndsWith (".empty"))
file_name = file_name.Substring (0, file_name.Length - 6);
2012-03-10 20:51:37 +00:00
message += "+ " + file_name + "" + n;
2011-04-20 14:02:20 +00:00
count++;
if (count == max_count)
return message + "...";
2011-04-20 14:02:20 +00:00
}
foreach (string modified in Modified) {
file_name = modified.Trim ("\"".ToCharArray ());
if (file_name.EndsWith (".empty"))
continue;
message += "/ " + file_name + "" + n;
2011-04-20 14:02:20 +00:00
count++;
if (count == max_count)
return message + "...";
2011-04-20 14:02:20 +00:00
}
foreach (string removed in Removed) {
file_name = removed.Trim ("\"".ToCharArray ());
if (file_name.EndsWith (".empty"))
file_name = file_name.Substring (0, file_name.Length - 6);
message += "- " + file_name + "" + n;
2011-04-20 14:02:20 +00:00
count++;
if (count == max_count)
return message + "..." + n;
2011-04-20 14:02:20 +00:00
}
message = message.Replace ("\"", "");
return message.TrimEnd ();
2011-04-20 14:02:20 +00:00
}
// Recursively gets a folder's size in bytes
2012-02-08 19:42:29 +00:00
private double CalculateSizes (DirectoryInfo parent)
{
2012-04-10 21:19:33 +00:00
if (!Directory.Exists (parent.FullName))
return 0;
2012-02-08 19:42:29 +00:00
if (parent.Name.Equals ("rebase-apply"))
return 0;
2012-04-10 21:19:33 +00:00
double size = 0;
try {
foreach (FileInfo file in parent.GetFiles ()) {
if (!file.Exists)
return 0;
if (file.Name.Equals (".empty"))
File.SetAttributes (file.FullName, FileAttributes.Hidden);
size += file.Length;
}
2012-04-10 21:19:33 +00:00
} catch (Exception e) {
SparkleHelpers.DebugInfo ("Local", "Error calculating size: " + e.Message);
return 0;
}
try {
foreach (DirectoryInfo directory in parent.GetDirectories ())
2012-02-08 19:42:29 +00:00
size += CalculateSizes (directory);
2012-04-10 21:19:33 +00:00
} catch (Exception e) {
SparkleHelpers.DebugInfo ("Local", "Error calculating size: " + e.Message);
return 0;
}
return size;
}
2011-04-20 14:02:20 +00:00
}
}