SparkleShare/SparkleLib/Git/SparkleRepoGit.cs

1031 lines
38 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;
2012-10-14 18:57:13 +00:00
using System.Threading;
2012-07-03 07:58:35 +00:00
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 use_git_bin;
2012-10-14 18:57:13 +00:00
private bool is_encrypted;
2012-07-14 10:45:54 +00:00
public SparkleRepo (string path, SparkleConfig config) : base (path, config)
2012-01-29 20:33:12 +00:00
{
// TODO: Set git locale to en-US
// Check if we should use git-bin
SparkleGit git = new SparkleGit (LocalPath, "config --get filter.bin.clean");
git.StartAndWaitForExit ();
this.use_git_bin = (git.ExitCode == 0);
git = new SparkleGit (LocalPath, "config remote.origin.url \"" + RemoteUrl + "\"");
git.StartAndWaitForExit ();
2012-07-28 13:58:09 +00:00
string rebase_apply_path = new string [] { LocalPath, ".git", "rebase-apply" }.Combine ();
if (Directory.Exists (rebase_apply_path)) {
2012-07-25 11:28:50 +00:00
git = new SparkleGit (LocalPath, "rebase --abort");
2012-07-26 10:12:14 +00:00
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 {
2012-07-18 12:49:11 +00:00
string file_path = new string [] { LocalPath, ".git", "repo_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 {
2012-07-18 12:49:11 +00:00
string file_path = new string [] { LocalPath, ".git", "repo_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")));
2012-07-18 12:49:11 +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 string [] UnsyncedFilePaths {
get {
List<string> file_paths = new List<string> ();
2012-07-26 10:12:14 +00:00
SparkleGit git = new SparkleGit (LocalPath, "status --porcelain");
string output = git.StartAndReadStandardOutput ();
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 ());
2012-07-18 12:49:11 +00:00
file_paths.Add (path);
}
}
return file_paths.ToArray ();
}
}
2011-05-19 16:05:58 +00:00
public override string CurrentRevision {
get {
SparkleGit git = new SparkleGit (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 {
2012-07-28 13:58:09 +00:00
SparkleLogger.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
SparkleGit git = new SparkleGit (LocalPath, "ls-remote --heads --exit-code \"" + RemoteUrl + "\" master");
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
2012-07-03 07:58:35 +00:00
string remote_revision = output.Substring (0, 40);
2012-06-09 15:27:34 +00:00
if (!remote_revision.StartsWith (current_revision)) {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("Git", Name + " | Remote changes found, local: " +
2012-07-18 12:49:11 +00:00
current_revision + ", remote: " + remote_revision);
Error = ErrorStatus.None;
return true;
2011-08-25 15:02:34 +00:00
} else {
2012-07-28 13:58:09 +00:00
SparkleLogger.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
{
2012-02-08 19:42:29 +00:00
if (HasLocalChanges) {
Add ();
string message = FormatCommitMessage ();
Commit (message);
string salt_file_path = new string [] { LocalPath, ".git", "salt" }.Combine ();
// If the repo is encrypted, create a branch to
// store the in and push it to the host
if (File.Exists (salt_file_path)) {
string salt = File.ReadAllText (salt_file_path).Trim ();
SparkleGit git_salt = new SparkleGit (LocalPath, "branch salt-" + salt);
git_salt.StartAndWaitForExit ();
git_salt = new SparkleGit (LocalPath, "push origin salt-" + salt);
git_salt.StartAndWaitForExit ();
File.Delete (salt_file_path);
}
}
2011-04-20 14:02:20 +00:00
SparkleGit git;
if (this.use_git_bin) {
SparkleGitBin git_bin = new SparkleGitBin (LocalPath, "push");
2012-07-26 10:12:14 +00:00
git_bin.StartAndWaitForExit ();
// 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 {
// "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 {
2012-07-28 13:58:09 +00:00
SparkleLogger.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 ();
if (git.ExitCode == 0) {
ClearCache ();
return true;
} else {
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
{
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 {
2012-07-28 13:58:09 +00:00
SparkleLogger.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) {
2011-05-19 16:05:58 +00:00
Rebase ();
2012-07-18 12:49:11 +00:00
string identifier_file_path = Path.Combine (LocalPath, ".sparkleshare");
File.SetAttributes (identifier_file_path, FileAttributes.Hidden);
2012-04-29 14:19:36 +00:00
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 {
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);
2011-04-20 14:02:20 +00:00
SparkleGit git = new SparkleGit (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 void Add ()
{
SparkleGit git = new SparkleGit (LocalPath, "add --all");
2012-07-26 10:12:14 +00:00
git.StartAndWaitForExit ();
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("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-07-22 09:40:49 +00:00
git = new SparkleGit (LocalPath, "config user.name \"" + base.local_config.User.Name + "\"");
2012-07-26 10:12:14 +00:00
git.StartAndWaitForExit ();
2012-07-22 09:40:49 +00:00
git = new SparkleGit (LocalPath, "config user.email \"" + base.local_config.User.Email + "\"");
2012-07-26 10:12:14 +00:00
git.StartAndWaitForExit ();
this.user_is_set = true;
}
2012-07-22 09:40:49 +00:00
git = new SparkleGit (LocalPath, "commit --all --message=\"" + message + "\" " +
"--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
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;
2012-07-26 10:12:14 +00:00
git.StartAndWaitForExit ();
2010-07-20 21:21:37 +00:00
if (git.ExitCode != 0) {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("Git", Name + " | Conflict detected, trying to get out...");
while (HasLocalChanges) {
try {
ResolveConflict ();
} catch (IOException e) {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("Git",
2012-07-02 21:58:37 +00:00
Name + " | Failed to resolve conflict, trying again... (" + e.Message + ")");
}
}
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("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 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
//
// 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' after this comment
2011-04-28 11:49:14 +00:00
SparkleGit git_status = new SparkleGit (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 ());
2012-07-02 21:10:03 +00:00
bool changes_added = 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);
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("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
2012-07-26 10:12:14 +00:00
SparkleGit git_theirs = new SparkleGit (LocalPath, "checkout --theirs \"" + conflicting_path + "\"");
git_theirs.StartAndWaitForExit ();
File.SetAttributes (Path.Combine (LocalPath, conflicting_path), FileAttributes.Hidden);
2012-07-02 21:10:03 +00:00
changes_added = true;
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
2012-07-22 09:40:49 +00:00
SparkleGit git_theirs = new SparkleGit (LocalPath, "checkout --theirs \"" + conflicting_path + "\"");
2012-07-26 10:12:14 +00:00
git_theirs.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 their_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_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
2012-07-22 09:40:49 +00:00
SparkleGit git_ours = new SparkleGit (LocalPath, "checkout --ours \"" + conflicting_path + "\"");
2012-07-26 10:12:14 +00:00
git_ours.StartAndWaitForExit ();
2011-04-28 11:49:14 +00:00
2012-07-02 21:10:03 +00:00
changes_added = true;
// 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
2012-07-22 09:40:49 +00:00
SparkleGit git_add = new SparkleGit (LocalPath, "add \"" + conflicting_path + "\"");
2012-07-26 10:12:14 +00:00
git_add.StartAndWaitForExit ();
2012-07-02 21:10:03 +00:00
changes_added = true;
}
2011-04-28 11:49:14 +00:00
}
2012-07-02 21:10:03 +00:00
Add ();
SparkleGit git;
if (changes_added)
git = new SparkleGit (LocalPath, "rebase --continue");
else
git = new SparkleGit (LocalPath, "rebase --skip");
git.StartInfo.RedirectStandardOutput = false;
2012-07-26 10:12:14 +00:00
git.StartAndWaitForExit ();
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");
path = path.Replace ("\\", "/");
2012-10-14 18:57:13 +00:00
SparkleLogger.LogInfo ("Git", Name + " | Restoring \"" + path + "\" (revision " + revision + ")");
2012-10-14 18:57:13 +00:00
// FIXME: git-show doesn't decrypt objects, so we can't use it to retrieve
// files from the index. This is a suboptimal workaround but it does the job
if (this.is_encrypted) {
// Restore the older file...
SparkleGit git = new SparkleGit (LocalPath, "checkout " + revision + " \"" + path + "\"");
git.StartAndWaitForExit ();
string local_file_path = Path.Combine (LocalPath, path);
// ...move it...
try {
File.Move (local_file_path, target_file_path);
} catch {
SparkleLogger.LogInfo ("Git",
Name + " | Could not move \"" + local_file_path + "\" to \"" + target_file_path + "\"");
}
// ...and restore the most recent revision
git = new SparkleGit (LocalPath, "checkout " + CurrentRevision + " \"" + path + "\"");
git.StartAndWaitForExit ();
// The correct way
} else {
path = path.Replace (" ", "\\ ");
SparkleGit git = new SparkleGit (LocalPath, "show " + revision + ":" + path + "");
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 ();
}
public override List<SparkleChangeSet> GetChangeSets (string path, int count)
{
return GetChangeSetsInternal (path, count);
}
2012-04-29 14:19:36 +00:00
public override List<SparkleChangeSet> GetChangeSets (int count)
{
return GetChangeSetsInternal (null, count);
}
private bool FindError (string line)
{
Error = ErrorStatus.None;
if (line.StartsWith ("WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!") ||
line.StartsWith ("WARNING: POSSIBLE DNS SPOOFING DETECTED!")) {
Error = ErrorStatus.HostIdentityChanged;
} else if (line.StartsWith ("Permission denied")) {
Error = ErrorStatus.AuthenticationFailed;
} else if (line.StartsWith ("error: Disk space exceeded")) {
Error = ErrorStatus.DiskSpaceExcedeed;
}
if (Error != ErrorStatus.None) {
SparkleLogger.LogInfo ("Git", Name + " | Error status changed to " + Error);
return true;
} else {
return false;
}
}
private List<SparkleChangeSet> GetChangeSetsInternal (string path, int count)
2011-04-20 14:02:20 +00:00
{
2011-07-23 20:34:04 +00:00
if (count < 1)
throw new ArgumentOutOfRangeException ("count");
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;
if (path == null) {
git = new SparkleGit (LocalPath, "log -" + count + " --raw --find-renames --date=iso " +
"--format=medium --no-color --no-merges");
2011-04-20 14:02:20 +00:00
} else {
path = path.Replace ("\\", "/");
git = new SparkleGit (LocalPath, "log -" + count + " --raw --find-renames --date=iso " +
"--format=medium --no-color --no-merges -- \"" + path + "\"");
}
string output = git.StartAndReadStandardOutput ();
string [] lines = output.Split ("\n".ToCharArray ());
List<string> entries = new List <string> ();
2011-04-20 14:02:20 +00:00
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 (":")) {
2012-07-26 10:41:19 +00:00
string type_letter = entry_line [37].ToString ();
2011-04-20 14:02:20 +00:00
string file_path = entry_line.Substring (39);
bool change_is_folder = false;
2011-09-14 15:57:40 +00:00
if (file_path.Equals (".sparkleshare"))
continue;
if (file_path.EndsWith (".empty")) {
file_path = file_path.Substring (0, file_path.Length - ".empty".Length);
change_is_folder = true;
}
2012-07-26 10:41:19 +00:00
file_path = EnsureSpecialCharacters (file_path);
2012-07-14 10:45:54 +00:00
file_path = file_path.Replace ("\\\"", "\"");
2012-07-26 18:17:01 +00:00
if (type_letter.Equals ("R")) {
2012-07-26 10:41:19 +00:00
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);
2012-07-01 08:46:42 +00:00
file_path = EnsureSpecialCharacters (file_path);
to_file_path = EnsureSpecialCharacters (to_file_path);
2012-07-14 10:45:54 +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;
}
if (to_file_path.EndsWith (".empty")) {
to_file_path = to_file_path.Substring (0, to_file_path.Length - 6);
change_is_folder = true;
}
change_set.Changes.Add (
new SparkleChange () {
Path = file_path,
IsFolder = change_is_folder,
MovedToPath = to_file_path,
Timestamp = change_set.Timestamp,
Type = SparkleChangeType.Moved
}
);
2012-07-26 10:41:19 +00:00
} else {
SparkleChangeType change_type = SparkleChangeType.Added;
if (type_letter.Equals ("M")) {
change_type = SparkleChangeType.Edited;
} else if (type_letter.Equals ("D")) {
change_type = SparkleChangeType.Deleted;
2012-07-26 10:41:19 +00:00
}
change_set.Changes.Add (
new SparkleChange () {
Path = file_path,
IsFolder = change_is_folder,
2012-07-26 10:41:19 +00:00
Timestamp = change_set.Timestamp,
Type = change_type
}
);
2011-04-20 14:02:20 +00:00
}
}
}
2011-05-19 16:05:58 +00:00
if (change_sets.Count > 0 && path == null) {
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;
} else {
last_change_set.FirstTimestamp = change_set.Timestamp;
}
} else {
change_sets.Add (change_set);
}
} else {
if (path != null) {
bool skip_change_set = false;;
foreach (SparkleChange change in change_set.Changes) {
if ((change.Type == SparkleChangeType.Deleted ||
change.Type == SparkleChangeType.Moved) && change.Path.Equals (path)) {
skip_change_set = true;
}
}
if (skip_change_set)
continue;
}
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
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");
2012-07-26 10:12:14 +00:00
git_bin.StartAndWaitForExit ();
}
// 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");
2012-07-28 13:58:09 +00:00
SparkleLogger.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 {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("Git", Name + " | Failed adding empty folder " + path);
}
}
}
} catch (IOException e) {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("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 ()
{
2012-07-18 12:49:11 +00:00
int count = 0;
string message = "";
SparkleGit git_status = new SparkleGit (LocalPath, "status --porcelain");
git_status.Start ();
while (!git_status.StandardOutput.EndOfStream) {
string line = git_status.StandardOutput.ReadLine ();
if (line.EndsWith (".empty") || line.EndsWith (".empty\""))
line = line.Replace (".empty", "");
2012-07-02 16:33:21 +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 ());
2012-07-18 12:49:11 +00:00
message += "< " + EnsureSpecialCharacters (path) + "\n";
message += "> " + EnsureSpecialCharacters (moved_to_path) + "\n";
} else {
if (line.StartsWith ("M")) {
message += "/";
} else if (line.StartsWith ("D")) {
message += "-";
2011-04-20 14:02:20 +00:00
} else {
message += "+";
}
2011-04-20 14:02:20 +00:00
string path = line.Substring (3).Trim ("\"".ToCharArray ());
2012-07-02 21:10:03 +00:00
message += " " + EnsureSpecialCharacters (path) + "\n";
}
2011-04-20 14:02:20 +00:00
count++;
if (count == 10) {
message += "...\n";
break;
}
2011-04-20 14:02:20 +00:00
}
git_status.StandardOutput.ReadToEnd ();
git_status.WaitForExit ();
2012-07-14 10:45:54 +00:00
return message;
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-07-18 12:49:11 +00:00
if (!Directory.Exists (parent.FullName) || 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) {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("Local", "Error calculating size: " + e.Message);
2012-04-10 21:19:33 +00:00
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) {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("Local", "Error calculating size: " + e.Message);
return 0;
}
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
}
}