SparkleShare/SparkleLib/SparkleRepo.cs

1064 lines
37 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SparkleShare, an instant update workflow to Git.
// 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
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Timers;
using Meebey.SmartIrc4net;
using Mono.Unix;
namespace SparkleLib {
public enum SyncStatus {
SyncUpStarted,
SyncUpFinished,
SyncUpFailed,
SyncDownStarted,
SyncDownFinished,
SyncDownFailed
}
public class SparkleRepo {
private Timer remote_timer;
private Timer local_timer;
private FileSystemWatcher watcher;
private System.Object change_lock;
private SparkleListenerBase listener;
private List <double> sizebuffer;
private bool has_changed;
private string current_hash;
private bool is_syncing;
private bool is_buffering;
private bool is_polling;
private bool is_fetching;
private bool is_pushing;
private bool has_unsynced_changes;
private bool server_online;
public readonly SparkleBackend Backend;
public readonly string Name;
public readonly string RemoteName;
public readonly string Domain;
public readonly string Description;
public readonly string LocalPath;
public readonly string RemoteOriginUrl;
public readonly string UserName;
public readonly string UserEmail;
public string CurrentHash {
get {
return this.current_hash;
}
}
public bool IsBuffering {
get {
return this.is_buffering;
}
}
public bool IsPushing {
get {
return this.is_pushing;
}
}
public bool IsPolling {
get {
return this.is_polling;
}
}
public bool IsSyncing {
get {
return this.is_syncing;
}
}
public bool IsFetching {
get {
return this.is_fetching;
}
}
public bool HasUnsyncedChanges {
get {
return this.has_unsynced_changes;
}
}
public bool ServerOnline {
get {
return this.server_online;
}
}
public delegate void SyncStatusChangedEventHandler (SyncStatus status);
public event SyncStatusChangedEventHandler SyncStatusChanged;
public delegate void NewCommitEventHandler (SparkleCommit commit, string repository_path);
public delegate void ConflictDetectedEventHandler (object o, SparkleEventArgs args);
public delegate void ChangesDetectedEventHandler (object o, SparkleEventArgs args);
public delegate void CommitEndedUpEmptyEventHandler (object o, SparkleEventArgs args);
public event NewCommitEventHandler NewCommit;
public event ConflictDetectedEventHandler ConflictDetected;
public event ChangesDetectedEventHandler ChangesDetected;
public event CommitEndedUpEmptyEventHandler CommitEndedUpEmpty;
public SparkleRepo (string path, SparkleBackend backend)
{
LocalPath = path;
Name = Path.GetFileName (LocalPath);
RemoteOriginUrl = GetRemoteOriginUrl ();
RemoteName = Path.GetFileNameWithoutExtension (RemoteOriginUrl);
Domain = GetDomain (RemoteOriginUrl);
Description = GetDescription ();
UserName = GetUserName ();
UserEmail = GetUserEmail ();
Backend = backend;
this.is_syncing = false;
this.is_buffering = false;
this.is_polling = true;
this.is_fetching = false;
this.is_pushing = false;
this.server_online = true;
this.has_changed = false;
this.change_lock = new Object ();
if (IsEmpty)
this.current_hash = null;
else
this.current_hash = GetCurrentHash ();
string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath,
".git", "has_unsynced_changes");
if (File.Exists (unsynced_file_path))
this.has_unsynced_changes = true;
else
this.has_unsynced_changes = false;
if (this.current_hash == null)
CreateInitialCommit ();
// Watch the repository's folder
this.watcher = new FileSystemWatcher (LocalPath) {
IncludeSubdirectories = true,
EnableRaisingEvents = true,
Filter = "*"
};
this.watcher.Changed += new FileSystemEventHandler (OnFileActivity);
this.watcher.Created += new FileSystemEventHandler (OnFileActivity);
this.watcher.Deleted += new FileSystemEventHandler (OnFileActivity);
this.watcher.Renamed += new RenamedEventHandler (OnFileActivity);
NotificationServerType server_type;
if (UsesNotificationCenter)
server_type = NotificationServerType.Central;
else
server_type = NotificationServerType.Own;
this.listener = new SparkleListenerIrc (Domain, Identifier, server_type);
// ...fetch remote changes every 60 seconds if that fails
this.remote_timer = new Timer () {
Interval = 60000
};
this.remote_timer.Elapsed += delegate {
if (this.is_polling) {
CheckForRemoteChanges ();
if (!this.listener.IsConnected)
this.listener.Connect ();
}
if (this.has_unsynced_changes)
FetchRebaseAndPush ();
};
// Stop polling when the connection to the irc channel is succesful
this.listener.Connected += delegate {
this.is_polling = false;
// Check for changes manually one more time
CheckForRemoteChanges ();
// Push changes that were made since the last disconnect
if (this.has_unsynced_changes)
Push ();
};
// Start polling when the connection to the irc channel is lost
this.listener.Disconnected += delegate {
SparkleHelpers.DebugInfo ("Local", "[" + Name + "] Falling back to polling");
this.is_polling = true;
};
// Fetch changes when there is a message in the irc channel
this.listener.RemoteChange += delegate (string change_id) {
if (!change_id.Equals (this.current_hash) && change_id.Length == 40) {
if (!this.is_fetching && !this.is_buffering) {
while (this.listener.ChangesQueue > 0) {
Fetch ();
this.listener.DecrementChangesQueue ();
}
this.watcher.EnableRaisingEvents = false;
Rebase ();
this.watcher.EnableRaisingEvents = true;
}
}
};
// Start listening
this.listener.Connect ();
this.sizebuffer = new List <double> ();
// Keep a timer that checks if there are changes and
// whether they have settled
this.local_timer = new Timer () {
Interval = 250
};
this.local_timer.Elapsed += delegate (object o, ElapsedEventArgs args) {
CheckForChanges ();
};
this.remote_timer.Start ();
this.local_timer.Start ();
// Add everything that changed
// since SparkleShare was stopped
AddCommitAndPush ();
if (this.current_hash == null)
this.current_hash = GetCurrentHash ();
}
public string Identifier {
get {
// Because git computes a hash based on content,
// author, and timestamp; it is unique enough to
// use the hash of the first commit as an identifier
// for our folder
SparkleGit git = new SparkleGit (LocalPath, "log --reverse -1 --format=%H");
git.Start ();
git.WaitForExit ();
return git.StandardOutput.ReadToEnd ().Trim ();
}
}
private void CheckForRemoteChanges ()
{
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Checking for remote changes...");
SparkleGit git = new SparkleGit (LocalPath, "ls-remote origin master");
git.Exited += delegate {
if (git.ExitCode != 0)
return;
string remote_hash = git.StandardOutput.ReadToEnd ().TrimEnd ();
if (!remote_hash.StartsWith (this.current_hash)) {
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Remote changes found. (" + remote_hash + ")");
Fetch ();
this.watcher.EnableRaisingEvents = false;
Rebase ();
this.watcher.EnableRaisingEvents = true;
}
};
git.Start ();
git.WaitForExit ();
}
private void CheckForChanges ()
{
lock (this.change_lock) {
if (this.has_changed) {
if ( this.sizebuffer.Count >= 4)
this.sizebuffer.RemoveAt (0);
DirectoryInfo dir_info = new DirectoryInfo (LocalPath);
this.sizebuffer.Add (CalculateFolderSize (dir_info));
if ( this.sizebuffer [0].Equals ( this.sizebuffer [1]) &&
this.sizebuffer [1].Equals ( this.sizebuffer [2]) &&
this.sizebuffer [2].Equals ( this.sizebuffer [3])) {
SparkleHelpers.DebugInfo ("Local", "[" + Name + "] Changes have settled.");
this.is_buffering = false;
this.has_changed = false;
while (AnyDifferences) {
this.watcher.EnableRaisingEvents = false;
AddCommitAndPush ();
this.watcher.EnableRaisingEvents = true;
}
}
}
}
}
// Starts a timer when something changes
private void OnFileActivity (object o, FileSystemEventArgs fse_args)
{
if (fse_args.Name.StartsWith (".git/"))
return;
WatcherChangeTypes wct = fse_args.ChangeType;
if (AnyDifferences) {
this.is_buffering = true;
// Only fire the event if the timer has been stopped.
// This prevents multiple events from being raised whilst "buffering".
if (!this.has_changed) {
SparkleEventArgs args = new SparkleEventArgs ("ChangesDetected");
if (ChangesDetected != null)
ChangesDetected (this, args);
}
SparkleHelpers.DebugInfo ("Event", "[" + Name + "] " + wct.ToString () + " '" + fse_args.Name + "'");
SparkleHelpers.DebugInfo ("Local", "[" + Name + "] Changes found, checking if settled.");
this.remote_timer.Stop ();
lock (this.change_lock) {
this.has_changed = true;
}
}
}
// When there are changes we generally want to Add, Commit and Push,
// so this method does them all with appropriate timers, etc. switched off
public void AddCommitAndPush ()
{
try {
this.local_timer.Stop ();
this.remote_timer.Stop ();
if (AnyDifferences) {
Add ();
string message = FormatCommitMessage ();
Commit (message);
Push ();
} else {
SparkleEventArgs args = new SparkleEventArgs ("CommitEndedUpEmpty");
if (CommitEndedUpEmpty != null)
CommitEndedUpEmpty (this, args);
}
} finally {
this.remote_timer.Start ();
this.local_timer.Start ();
}
}
public void FetchRebaseAndPush ()
{
CheckForRemoteChanges ();
Push ();
}
private bool AnyDifferences {
get {
SparkleGit git = new SparkleGit (LocalPath, "status --porcelain");
git.Start ();
git.WaitForExit ();
string output = git.StandardOutput.ReadToEnd ().TrimEnd ();
string [] lines = output.Split ("\n".ToCharArray ());
foreach (string line in lines) {
if (line.Length > 1 && !line [1].Equals (" "))
return true;
}
return false;
}
}
private bool IsEmpty {
get {
SparkleGit git = new SparkleGit (LocalPath, "log -1");
git.Start ();
git.WaitForExit ();
return (git.ExitCode != 0);
}
}
private string GetCurrentHash ()
{
// Remove stale rebase-apply files because it
// makes the method return the wrong hashes.
string rebase_apply_file = SparkleHelpers.CombineMore (LocalPath, ".git", "rebase-apply");
if (File.Exists (rebase_apply_file))
File.Delete (rebase_apply_file);
SparkleGit git = new SparkleGit (LocalPath, "log -1 --format=%H");
git.Start ();
git.WaitForExit ();
string output = git.StandardOutput.ReadToEnd ();
string hash = output.Trim ();
return hash;
}
// Stages the made changes
private void Add ()
{
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Staging changes...");
SparkleGit git = new SparkleGit (LocalPath, "add --all");
git.Start ();
git.WaitForExit ();
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes staged.");
}
// Removes unneeded objects
private void CollectGarbage ()
{
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Collecting garbage...");
SparkleGit git = new SparkleGit (LocalPath, "gc");
git.Start ();
git.WaitForExit ();
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Garbage collected.");
}
// Commits the made changes
public void Commit (string message)
{
if (!AnyDifferences)
return;
SparkleGit git = new SparkleGit (LocalPath, "commit -m '" + message + "'");
git.Start ();
git.WaitForExit ();
this.current_hash = GetCurrentHash ();
SparkleHelpers.DebugInfo ("Commit", "[" + Name + "] " + message + " (" + this.current_hash + ")");
// Collect garbage pseudo-randomly
if (DateTime.Now.Second % 10 == 0)
CollectGarbage ();
}
// Fetches changes from the remote repository
public void Fetch ()
{
this.is_syncing = true;
this.is_fetching = true;
this.remote_timer.Stop ();
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Fetching changes...");
SparkleGit git = new SparkleGit (LocalPath, "fetch -v origin master");
if (SyncStatusChanged != null)
SyncStatusChanged (SyncStatus.SyncDownStarted);
git.Exited += delegate {
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes fetched.");
this.is_syncing = false;
this.is_fetching = false;
this.current_hash = GetCurrentHash ();
if (git.ExitCode != 0) {
this.server_online = false;
if (SyncStatusChanged != null)
SyncStatusChanged (SyncStatus.SyncDownFailed);
} else {
this.server_online = true;
if (SyncStatusChanged != null)
SyncStatusChanged (SyncStatus.SyncDownFinished);
}
this.remote_timer.Start ();
};
git.Start ();
git.WaitForExit ();
}
// Merges the fetched changes
public void Rebase ()
{
if (AnyDifferences) {
Add ();
string commit_message = FormatCommitMessage ();
Commit (commit_message);
}
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Rebasing changes...");
SparkleGit git = new SparkleGit (LocalPath, "rebase -v FETCH_HEAD");
git.Exited += delegate {
if (git.ExitCode != 0) {
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Conflict detected. Trying to get out...");
DisableWatching ();
while (AnyDifferences)
ResolveConflict ();
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Conflict resolved.");
EnableWatching ();
SparkleEventArgs args = new SparkleEventArgs ("ConflictDetected");
if (ConflictDetected != null)
ConflictDetected (this, args);
Push ();
}
this.current_hash = GetCurrentHash ();
};
git.Start ();
git.WaitForExit ();
this.current_hash = GetCurrentHash ();
if (NewCommit != null)
NewCommit (GetCommits (1) [0], LocalPath);
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes rebased.");
}
private void ResolveConflict ()
{
// This is al 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
//
// 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'
SparkleGit git_status = new SparkleGit (LocalPath, "status --porcelain");
git_status.Start ();
git_status.WaitForExit ();
string output = git_status.StandardOutput.ReadToEnd ().TrimEnd ();
string [] lines = output.Split ("\n".ToCharArray ());
foreach (string line in lines) {
string conflicting_path = line.Substring (3);
conflicting_path = conflicting_path.Trim ("\"".ToCharArray ());
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Conflict type: " + line);
// 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 ();
// Append a timestamp to local version
string timestamp = DateTime.Now.ToString ("HH:mm MMM d");
string their_path = conflicting_path + " (" + UserName + ", " + timestamp + ")";
string abs_conflicting_path = Path.Combine (LocalPath, conflicting_path);
string abs_their_path = Path.Combine (LocalPath, their_path);
File.Move (abs_conflicting_path, abs_their_path);
// Recover server version
SparkleGit git_ours = new SparkleGit (LocalPath,
"checkout --ours \"" + conflicting_path + "\"");
git_ours.Start ();
git_ours.WaitForExit ();
Add ();
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
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
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 ();
}
}
}
// Pushes the changes to the remote repo
public void Push ()
{
this.is_syncing = true;
this.is_pushing = true;
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Pushing changes...");
SparkleGit git = new SparkleGit (LocalPath, "push origin master");
if (SyncStatusChanged != null)
SyncStatusChanged (SyncStatus.SyncUpStarted);
git.Exited += delegate {
this.is_syncing = false;
this.is_pushing = false;
if (git.ExitCode != 0) {
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Pushing failed.");
string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath ,
".git", "has_unsynced_changes");
if (!File.Exists (unsynced_file_path))
File.Create (unsynced_file_path);
this.has_unsynced_changes = true;
if (SyncStatusChanged != null)
SyncStatusChanged (SyncStatus.SyncUpFailed);
FetchRebaseAndPush ();
} else {
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes pushed.");
string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath ,
".git", "has_unsynced_changes");
if (File.Exists (unsynced_file_path))
File.Delete (unsynced_file_path);
this.has_unsynced_changes = false;
if (SyncStatusChanged != null)
SyncStatusChanged (SyncStatus.SyncDownFinished);
this.listener.Announce (this.current_hash);
}
};
git.Start ();
git.WaitForExit ();
}
public void DisableWatching ()
{
this.watcher.EnableRaisingEvents = false;
}
public void EnableWatching ()
{
this.watcher.EnableRaisingEvents = true;
}
// Gets the domain name of a given URL
// TODO: make this a regex
private string GetDomain (string url)
{
if (url.Equals (""))
return null;
string domain = url.Substring (url.IndexOf ("@") + 1);
if (domain.Contains (":"))
domain = domain.Substring (0, domain.IndexOf (":"));
else
domain = domain.Substring (0, domain.IndexOf ("/"));
return domain;
}
// Gets the repository's description
private string GetDescription ()
{
string description_file_path = SparkleHelpers.CombineMore (LocalPath, ".git", "description");
if (!File.Exists (description_file_path))
return null;
StreamReader reader = new StreamReader (description_file_path);
string description = reader.ReadToEnd ();
reader.Close ();
if (description.StartsWith ("Unnamed"))
description = null;
return description;
}
private string GetRemoteOriginUrl ()
{
SparkleGit git = new SparkleGit (LocalPath, "config --get remote.origin.url");
git.Start ();
git.WaitForExit ();
string output = git.StandardOutput.ReadToEnd ();
string url = output.Trim ();
return url;
}
private string GetUserName ()
{
SparkleGit git = new SparkleGit (LocalPath, "config --get user.name");
git.Start ();
git.WaitForExit ();
string output = git.StandardOutput.ReadToEnd ();
string user_name = output.Trim ();
return user_name;
}
private string GetUserEmail ()
{
SparkleGit git = new SparkleGit (LocalPath, "config --get user.email");
git.Start ();
git.WaitForExit ();
string output = git.StandardOutput.ReadToEnd ();
string user_email = output.Trim ();
return user_email;
}
// Recursively gets a folder's size in bytes
private double CalculateFolderSize (DirectoryInfo parent)
{
if (!System.IO.Directory.Exists (parent.ToString ()))
return 0;
double size = 0;
// Ignore the temporary 'rebase-apply' directory. This prevents potential
// crashes when files are being queried whilst the files have already been deleted.
if (parent.Name.Equals ("rebase-apply"))
return 0;
foreach (FileInfo file in parent.GetFiles()) {
if (!file.Exists)
return 0;
size += file.Length;
}
foreach (DirectoryInfo directory in parent.GetDirectories())
size += CalculateFolderSize (directory);
return size;
}
// Create a first commit in case the user has cloned
// an empty repository
private void CreateInitialCommit ()
{
TextWriter writer = new StreamWriter (Path.Combine (LocalPath, "SparkleShare.txt"));
writer.WriteLine (":)");
writer.Close ();
}
// Returns a list of latest commits
// TODO: Method needs to be made a lot faster
public List <SparkleCommit> GetCommits (int count)
{
if (count < 1)
count = 30;
List <SparkleCommit> commits = new List <SparkleCommit> ();
SparkleGit git_log = new SparkleGit (LocalPath, "log -" + count + " --raw -M --date=iso");
Console.OutputEncoding = System.Text.Encoding.Unicode;
git_log.Start ();
// 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 j = 0;
string entry = "", last_entry = "";
foreach (string line in lines) {
if (line.StartsWith ("commit") && j > 0) {
entries.Add (entry);
entry = "";
}
entry += line + "\n";
j++;
last_entry = entry;
}
entries.Add (last_entry);
Regex merge_regex = new Regex (@"commit ([a-z0-9]{40})\n" +
"Merge: .+ .+\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);
Regex non_merge_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);
// TODO: Need to optimise for speed
foreach (string log_entry in entries) {
Regex regex;
bool is_merge_commit = false;
if (log_entry.Contains ("\nMerge: ")) {
regex = merge_regex;
is_merge_commit = true;
} else {
regex = non_merge_regex;
}
Match match = regex.Match (log_entry);
if (match.Success) {
SparkleCommit commit = new SparkleCommit ();
commit.Hash = match.Groups [1].Value;
commit.UserName = match.Groups [2].Value;
commit.UserEmail = match.Groups [3].Value;
commit.IsMerge = is_merge_commit;
commit.DateTime = 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));
string [] entry_lines = log_entry.Split ("\n".ToCharArray ());
foreach (string entry_line in entry_lines) {
if (entry_line.StartsWith (":")) {
string change_type = entry_line [37].ToString ();
string file_path = entry_line.Substring (39);
string to_file_path;
if (change_type.Equals ("A")) {
commit.Added.Add (file_path);
} else if (change_type.Equals ("M")) {
commit.Edited.Add (file_path);
} else if (change_type.Equals ("D")) {
commit.Deleted.Add (file_path);
} 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);
commit.MovedFrom.Add (file_path);
commit.MovedTo.Add (to_file_path);
}
}
}
commits.Add (commit);
}
}
return commits;
}
// 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 = null;
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 (2));
else if (line.StartsWith ("M"))
Modified.Add (line.Substring (2));
else if (line.StartsWith ("D"))
Removed.Add (line.Substring (2));
else if (line.StartsWith ("R")) {
Removed.Add (line.Substring (3, (line.IndexOf (" -> ") - 3)));
Added.Add (line.Substring (line.IndexOf (" -> ") + 4));
}
}
if (Added.Count > 0) {
foreach (string added in Added) {
file_name = added.Trim ("\"".ToCharArray ());
break;
}
message = "+ " + file_name + "";
}
if (Modified.Count > 0) {
foreach (string modified in Modified) {
file_name = modified.Trim ("\"".ToCharArray ());
break;
}
message = "/ " + file_name + "";
}
if (Removed.Count > 0) {
foreach (string removed in Removed) {
file_name = removed.Trim ("\"".ToCharArray ());
break;
}
message = "- " + file_name + "";
}
int changes_count = (Added.Count +
Modified.Count +
Removed.Count);
if (changes_count > 1)
message += " + " + (changes_count - 1);
return message;
}
public static bool IsRepo (string path)
{
return System.IO.Directory.Exists (Path.Combine (path, ".git"));
}
public bool UsesNotificationCenter
{
get {
string file_path = SparkleHelpers.CombineMore (LocalPath, ".git", "disable_notification_center");
return !File.Exists (file_path);
}
}
// Disposes all resourses of this object
public void Dispose ()
{
this.remote_timer.Dispose ();
this.local_timer.Dispose ();
this.listener.Dispose ();
}
}
}