SparkleShare/SparkleLib/SparkleRepo.cs

703 lines
17 KiB
C#
Raw Normal View History

// 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
2010-07-22 21:10:38 +00:00
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using Mono.Unix;
using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Timers;
namespace SparkleLib {
public class SparkleRepo
{
private Process Process;
private Timer FetchTimer;
private Timer BufferTimer;
private FileSystemWatcher Watcher;
private bool HasChanged;
private DateTime LastChange;
private System.Object ChangeLock = new System.Object();
public string Name;
public string Domain;
public string LocalPath;
public string RemoteOriginUrl;
public string CurrentHash;
public string UserEmail;
public string UserName;
public delegate void AddedEventHandler (object o, SparkleEventArgs args);
2010-07-21 23:17:20 +00:00
public delegate void CommitedEventHandler (object o, SparkleEventArgs args);
public delegate void PushingStartedEventHandler (object o, SparkleEventArgs args);
public delegate void PushingFinishedEventHandler (object o, SparkleEventArgs args);
2010-07-22 21:10:38 +00:00
public delegate void FetchingStartedEventHandler (object o, SparkleEventArgs args);
public delegate void FetchingFinishedEventHandler (object o, SparkleEventArgs args);
public delegate void NewCommitEventHandler (object o, NewCommitArgs args);
2010-07-24 14:03:58 +00:00
public delegate void ConflictDetectedEventHandler (object o, SparkleEventArgs args);
2010-07-21 23:17:20 +00:00
2010-07-24 14:03:58 +00:00
public event AddedEventHandler Added;
public event CommitedEventHandler Commited;
public event PushingStartedEventHandler PushingStarted;
public event PushingFinishedEventHandler PushingFinished;
public event FetchingStartedEventHandler FetchingStarted;
public event FetchingFinishedEventHandler FetchingFinished;
public event NewCommitEventHandler NewCommit;
public event ConflictDetectedEventHandler ConflictDetected;
2010-07-20 21:21:37 +00:00
public SparkleRepo (string path)
{
2010-07-31 19:13:01 +00:00
if (!Directory.Exists (path))
Directory.CreateDirectory (path);
2010-07-29 13:47:09 +00:00
2010-07-20 21:21:37 +00:00
LocalPath = path;
Name = Path.GetFileName (LocalPath);
2010-07-27 13:49:48 +00:00
Process = new Process () {
EnableRaisingEvents = true
};
Process.StartInfo.FileName = "git";
Process.StartInfo.RedirectStandardOutput = true;
Process.StartInfo.UseShellExecute = false;
2010-07-20 21:21:37 +00:00
Process.StartInfo.WorkingDirectory = LocalPath;
2010-07-20 21:21:37 +00:00
UserName = GetUserName ();
UserEmail = GetUserEmail ();
RemoteOriginUrl = GetRemoteOriginUrl ();
CurrentHash = GetCurrentHash ();
Domain = GetDomain (RemoteOriginUrl);
2010-07-21 23:17:20 +00:00
HasChanged = false;
// Watch the repository's folder
2010-07-20 21:21:37 +00:00
Watcher = new FileSystemWatcher (LocalPath) {
IncludeSubdirectories = true,
EnableRaisingEvents = true,
Filter = "*"
};
Watcher.Changed += new FileSystemEventHandler (OnFileActivity);
Watcher.Created += new FileSystemEventHandler (OnFileActivity);
Watcher.Deleted += new FileSystemEventHandler (OnFileActivity);
2010-07-21 23:17:20 +00:00
2010-07-24 21:31:24 +00:00
// Fetch remote changes every minute
2010-07-20 21:21:37 +00:00
FetchTimer = new Timer () {
2010-08-03 08:45:56 +00:00
Interval = 60000
2010-07-20 21:21:37 +00:00
};
FetchTimer.Elapsed += delegate {
Fetch ();
};
2010-07-21 23:17:20 +00:00
2010-07-20 21:21:37 +00:00
// Keep a buffer that checks if there are changes and
// whether they have settled
BufferTimer = new Timer () {
Interval = 4000
};
BufferTimer.Elapsed += delegate (object o, ElapsedEventArgs args) {
2010-07-20 21:21:37 +00:00
CheckForChanges ();
};
2010-07-20 21:21:37 +00:00
FetchTimer.Start ();
BufferTimer.Start ();
2010-07-21 23:17:20 +00:00
// Add everything that changed
// since SparkleShare was stopped
AddCommitAndPush ();
2010-07-20 21:21:37 +00:00
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Idling...");
}
private void CheckForChanges ()
{
lock (ChangeLock) {
if (HasChanged) {
SparkleHelpers.DebugInfo ("Buffer", "[" + Name + "] Changes found, checking if settled.");
DateTime now = DateTime.UtcNow;
TimeSpan changed = new TimeSpan (now.Ticks - LastChange.Ticks);
if (changed.TotalMilliseconds > 5000) {
HasChanged = false;
SparkleHelpers.DebugInfo ("Buffer", "[" + Name + "] Changes have settled, adding files...");
AddCommitAndPush ();
}
}
}
}
2010-07-20 21:21:37 +00:00
// Starts a time buffer when something changes
private void OnFileActivity (object o, FileSystemEventArgs args)
{
2010-07-20 21:21:37 +00:00
WatcherChangeTypes wct = args.ChangeType;
2010-07-20 21:21:37 +00:00
if (!ShouldIgnore (args.Name)) {
2010-07-20 21:21:37 +00:00
SparkleHelpers.DebugInfo ("Event", "[" + Name + "] " + wct.ToString () + " '" + args.Name + "'");
2010-07-20 21:21:37 +00:00
FetchTimer.Stop ();
2010-07-20 21:21:37 +00:00
lock (ChangeLock) {
2010-07-24 14:03:58 +00:00
LastChange = DateTime.UtcNow;
HasChanged = true;
2010-07-24 14:03:58 +00:00
}
2010-07-20 21:21:37 +00:00
}
2010-07-20 21:21:37 +00:00
}
2010-07-20 21:21:37 +00:00
// When there are changes we generally want to Add, Commit and Push
2010-07-20 21:21:37 +00:00
// so this method does them all with appropriate timers, etc. switched off
public void AddCommitAndPush ()
{
2010-07-20 21:21:37 +00:00
try {
2010-07-20 21:21:37 +00:00
BufferTimer.Stop ();
FetchTimer.Stop ();
Add ();
2010-07-24 14:03:58 +00:00
2010-07-20 21:21:37 +00:00
string message = FormatCommitMessage ();
if (!message.Equals ("")) {
2010-07-24 14:03:58 +00:00
2010-07-20 21:21:37 +00:00
Commit (message);
Fetch ();
Push ();
2010-07-24 14:03:58 +00:00
}
2010-07-20 21:21:37 +00:00
} finally {
FetchTimer.Start ();
BufferTimer.Start ();
2010-07-20 21:21:37 +00:00
}
}
2010-07-20 21:21:37 +00:00
// Stages the made changes
private void Add ()
{
2010-07-20 21:21:37 +00:00
// TODO: Check whether adding files is neccassary
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Staging changes...");
2010-07-20 21:21:37 +00:00
Process.StartInfo.Arguments = "add --all";
Process.Start ();
Process.WaitForExit ();
2010-07-20 21:21:37 +00:00
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes staged.");
2010-07-20 21:21:37 +00:00
2010-07-21 23:17:20 +00:00
SparkleEventArgs args = new SparkleEventArgs ("Added");
if (Added != null)
Added (this, args);
}
2010-07-20 21:21:37 +00:00
// Commits the made changes
2010-07-24 14:03:58 +00:00
public void Commit (string message)
{
2010-07-20 21:21:37 +00:00
2010-07-24 14:03:58 +00:00
SparkleHelpers.DebugInfo ("Commit", "[" + Name + "] " + message);
2010-07-20 21:21:37 +00:00
2010-07-24 14:03:58 +00:00
Process.StartInfo.Arguments = "commit -m \"" + message + "\"";
Process.Start ();
Process.WaitForExit ();
2010-07-20 21:21:37 +00:00
2010-07-21 23:17:20 +00:00
SparkleEventArgs args = new SparkleEventArgs ("Commited");
2010-07-24 14:03:58 +00:00
args.Message = message;
2010-07-21 23:17:20 +00:00
if (Commited != null)
2010-07-24 14:03:58 +00:00
Commited (this, args);
2010-07-21 23:17:20 +00:00
}
2010-07-20 21:21:37 +00:00
2010-07-24 21:31:24 +00:00
// Fetches changes from the remote repository
public void Fetch ()
{
2010-07-20 21:21:37 +00:00
FetchTimer.Stop ();
2010-07-20 21:21:37 +00:00
Process process = new Process () {
EnableRaisingEvents = true
};
2010-07-20 21:21:37 +00:00
process.StartInfo.FileName = "git";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = LocalPath;
2010-07-22 21:10:38 +00:00
SparkleEventArgs args;
args = new SparkleEventArgs ("FetchingStarted");
2010-07-20 21:21:37 +00:00
if (FetchingStarted != null)
FetchingStarted (this, args);
2010-07-20 21:21:37 +00:00
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Fetching changes...");
2010-07-27 11:00:01 +00:00
process.StartInfo.Arguments = "fetch";
2010-07-20 21:21:37 +00:00
process.Start ();
process.Exited += delegate {
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes fetched.");
2010-07-20 21:21:37 +00:00
// TODO: this doesn't exit sometimes
2010-07-21 23:17:20 +00:00
args = new SparkleEventArgs ("FetchingFinished");
2010-07-21 23:17:20 +00:00
if (FetchingFinished != null)
FetchingFinished (this, args);
2010-07-20 21:21:37 +00:00
Rebase ();
2010-07-20 21:21:37 +00:00
FetchTimer.Start ();
2010-07-20 21:21:37 +00:00
};
2010-07-20 21:21:37 +00:00
}
2010-07-21 23:17:20 +00:00
// Merges the fetched changes
public void Rebase ()
{
Watcher.EnableRaisingEvents = false;
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Rebasing changes...");
2010-07-20 21:21:37 +00:00
Process.StartInfo.Arguments = "rebase -v origin/master";
Process.WaitForExit ();
Process.Start ();
2010-07-20 21:21:37 +00:00
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes rebased.");
2010-07-20 21:21:37 +00:00
string output = Process.StandardOutput.ReadToEnd ().Trim ();
2010-07-20 21:21:37 +00:00
if (!output.Contains ("up to date")) {
2010-07-20 21:21:37 +00:00
if (output.Contains ("Failed to merge")) {
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Resolving conflict...");
Process.StartInfo.Arguments = "status";
Process.WaitForExit ();
Process.Start ();
2010-07-20 21:21:37 +00:00
output = Process.StandardOutput.ReadToEnd ().Trim ();
string [] lines = Regex.Split (output, "\n");
2010-07-20 21:21:37 +00:00
foreach (string line in lines) {
2010-07-20 21:21:37 +00:00
if (line.Contains ("needs merge")) {
2010-07-20 21:21:37 +00:00
string problem_file_name = line.Substring (line.IndexOf (": needs merge"));
2010-07-20 21:21:37 +00:00
Process.StartInfo.Arguments = "checkout --ours " + problem_file_name;
Process.WaitForExit ();
Process.Start ();
2010-07-24 14:03:58 +00:00
string timestamp = DateTime.Now.ToString ("H:mm d MMM yyyy");
2010-07-24 14:03:58 +00:00
File.Move (problem_file_name, problem_file_name + " (" + UserName + ", " + timestamp + ")");
2010-07-24 14:03:58 +00:00
Process.StartInfo.Arguments = "checkout --theirs " + problem_file_name;
Process.WaitForExit ();
Process.Start ();
2010-07-24 14:03:58 +00:00
SparkleEventArgs args = new SparkleEventArgs ("ConflictDetected");
2010-07-24 14:03:58 +00:00
if (ConflictDetected != null)
ConflictDetected (this, args);
}
}
Add ();
Process.StartInfo.Arguments = "rebase --continue";
Process.WaitForExit ();
Process.Start ();
2010-07-20 21:21:37 +00:00
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Conflict resolved.");
2010-07-20 21:21:37 +00:00
Push ();
Fetch ();
}
// Get the last commiter
Process.StartInfo.Arguments = "log --format=\"%an\" -1";
Process.Start ();
string author = Process.StandardOutput.ReadToEnd ().Trim ();
// Get the last committer e-mail
Process.StartInfo.Arguments = "log --format=\"%ae\" -1";
Process.Start ();
string email = Process.StandardOutput.ReadToEnd ().Trim ();
// Get the last commit message
Process.StartInfo.Arguments = "log --format=\"%s\" -1";
Process.Start ();
string message = Process.StandardOutput.ReadToEnd ().Trim ();
2010-07-24 14:03:58 +00:00
NewCommitArgs new_commit_args = new NewCommitArgs (author, email, message);
if (NewCommit != null)
2010-07-24 14:03:58 +00:00
NewCommit (this, new_commit_args);
}
Watcher.EnableRaisingEvents = true;
2010-07-20 21:21:37 +00:00
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Idling...");
}
2010-07-21 23:17:20 +00:00
// Pushes the changes to the remote repo
public void Push ()
{
2010-07-20 21:21:37 +00:00
SparkleEventArgs args = new SparkleEventArgs ("PushingStarted");
if (PushingStarted != null)
PushingStarted (this, args);
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Pushing changes...");
2010-07-20 21:21:37 +00:00
Process.StartInfo.Arguments = "push";
Process.Start ();
Process.WaitForExit ();
2010-07-20 21:21:37 +00:00
Process.Exited += delegate {
SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes pushed.");
2010-07-20 21:21:37 +00:00
args = new SparkleEventArgs ("PushingFinished");
2010-07-21 23:17:20 +00:00
if (PushingFinished != null)
PushingFinished (this, args);
2010-07-21 23:17:20 +00:00
};
2010-07-20 21:21:37 +00:00
}
2010-07-20 21:21:37 +00:00
public void Stop ()
{
FetchTimer.Stop ();
BufferTimer.Stop ();
}
2010-07-20 21:21:37 +00:00
// Ignores repos, dotfiles, swap files and the like.
private bool ShouldIgnore (string file_name)
{
2010-07-20 21:21:37 +00:00
2010-07-24 21:31:24 +00:00
if (file_name [0].Equals (".") ||
2010-07-20 21:21:37 +00:00
file_name.Contains (".lock") ||
file_name.Contains (".git") ||
file_name.Contains ("/.") ||
Directory.Exists (LocalPath + file_name)) {
return true; // Yes, ignore it.
} else if (file_name.Length > 3 &&
file_name.Substring (file_name.Length - 4).Equals (".swp")) {
return true; // Yes, ignore it.
2010-07-20 21:21:37 +00:00
} else {
return false;
}
}
2010-07-20 21:21:37 +00:00
2010-07-24 21:31:24 +00:00
// Gets the domain name of a given URL
2010-07-21 23:17:20 +00:00
public string GetDomain (string url)
{
2010-07-24 21:31:24 +00:00
string domain = url.Substring (RemoteOriginUrl.IndexOf ("@") + 1);
2010-07-21 23:17:20 +00:00
if (domain.IndexOf (":") > -1)
domain = domain.Substring (0, domain.IndexOf (":"));
else
domain = domain.Substring (0, domain.IndexOf ("/"));
return domain;
}
// Gets hash of the current commit
public string GetCurrentHash ()
{
string current_hash;
Process process = new Process ();
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = "git";
process.StartInfo.WorkingDirectory = LocalPath;
2010-07-21 23:17:20 +00:00
process.StartInfo.Arguments = "rev-list --max-count=1 HEAD";
process.Start ();
current_hash = process.StandardOutput.ReadToEnd ().Trim ();
return current_hash;
}
// Gets the user's name, example: "User Name"
public string GetUserName ()
{
string user_name;
Process process = new Process ();
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = "git";
process.StartInfo.WorkingDirectory = LocalPath;
2010-07-21 23:17:20 +00:00
process.StartInfo.Arguments = "config --get user.name";
process.Start ();
user_name = process.StandardOutput.ReadToEnd ().Trim ();
if (user_name.Equals ("")) {
UnixUserInfo unix_user_info = new UnixUserInfo (UnixEnvironment.UserName);
if (unix_user_info.RealName.Equals (""))
user_name = "???";
else
user_name = unix_user_info.RealName;
}
return user_name;
}
// Gets the user's email, example: "person@gnome.org"
public string GetUserEmail ()
{
string user_email;
Process process = new Process ();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.FileName = "git";
process.StartInfo.WorkingDirectory = LocalPath;
2010-07-21 23:17:20 +00:00
process.StartInfo.Arguments = "config --get user.email";
process.Start ();
user_email = process.StandardOutput.ReadToEnd ().Trim ();
return user_email;
}
// Gets the url of the remote repo, example: "ssh://git@git.gnome.org/project"
public string GetRemoteOriginUrl ()
{
string remote_origin_url;
Process process = new Process ();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.FileName = "git";
process.StartInfo.WorkingDirectory = LocalPath;
2010-07-21 23:17:20 +00:00
process.StartInfo.Arguments = "config --get remote.origin.url";
process.Start ();
remote_origin_url = process.StandardOutput.ReadToEnd ().Trim ();
return remote_origin_url;
}
// Creates a pretty commit message based on what has changed
private string FormatCommitMessage ()
{
bool DoneAddCommit = false;
bool DoneEditCommit = false;
bool DoneRenameCommit = false;
bool DoneDeleteCommit = false;
int FilesAdded = 0;
int FilesEdited = 0;
int FilesRenamed = 0;
int FilesDeleted = 0;
Process.StartInfo.Arguments = "status";
Process.Start ();
string Output = Process.StandardOutput.ReadToEnd ();
2010-07-20 21:21:37 +00:00
foreach (string line in Regex.Split (Output, "\n")) {
if (line.IndexOf ("new file:") > -1)
FilesAdded++;
2010-07-20 21:21:37 +00:00
if (line.IndexOf ("modified:") > -1)
FilesEdited++;
2010-07-20 21:21:37 +00:00
if (line.IndexOf ("renamed:") > -1)
FilesRenamed++;
2010-07-20 21:21:37 +00:00
if (line.IndexOf ("deleted:") > -1)
FilesDeleted++;
}
2010-07-20 21:21:37 +00:00
foreach (string line in Regex.Split (Output, "\n")) {
// Format message for when files are added,
// example: "added 'file' and 3 more."
2010-07-20 21:21:37 +00:00
if (line.IndexOf ("new file:") > -1 && !DoneAddCommit) {
DoneAddCommit = true;
if (FilesAdded > 1)
return "added " +
2010-07-20 21:21:37 +00:00
line.Replace ("#\tnew file:", "").Trim () +
"\nand " + (FilesAdded - 1) + " more.";
else
return "added " +
2010-07-20 21:21:37 +00:00
line.Replace ("#\tnew file:", "").Trim () + ".";
}
// Format message for when files are edited,
// example: "edited 'file'."
2010-07-20 21:21:37 +00:00
if (line.IndexOf ("modified:") > -1 && !DoneEditCommit) {
DoneEditCommit = true;
if (FilesEdited > 1)
return "edited " +
2010-07-20 21:21:37 +00:00
line.Replace ("#\tmodified:", "").Trim () +
"\nand " + (FilesEdited - 1) + " more.";
else
return "edited " +
2010-07-20 21:21:37 +00:00
line.Replace ("#\tmodified:", "").Trim () + ".";
}
// Format message for when files are edited,
// example: "deleted 'file'."
2010-07-20 21:21:37 +00:00
if (line.IndexOf ("deleted:") > -1 && !DoneDeleteCommit) {
DoneDeleteCommit = true;
if (FilesDeleted > 1)
return "deleted " +
2010-07-20 21:21:37 +00:00
line.Replace ("#\tdeleted:", "").Trim () +
"\nand " + (FilesDeleted - 1) + " more.";
else
return "deleted " +
2010-07-20 21:21:37 +00:00
line.Replace ("#\tdeleted:", "").Trim () + ".";
}
// Format message for when files are renamed,
// example: "renamed 'file' to 'new name'."
2010-07-20 21:21:37 +00:00
if (line.IndexOf ("renamed:") > -1 && !DoneRenameCommit) {
DoneDeleteCommit = true;
if (FilesRenamed > 1)
return "renamed " +
2010-07-20 21:21:37 +00:00
line.Replace ("#\trenamed:", "").Trim ().Replace
(" -> ", " to ") + " and " + (FilesDeleted - 1) +
" more.";
else
return "renamed " +
2010-07-20 21:21:37 +00:00
line.Replace ("#\trenamed:", "").Trim ().Replace
(" -> ", " to ") + ".";
}
}
// Nothing happened:
return "";
}
}
2010-07-24 21:31:24 +00:00
// Arguments for most events
2010-07-21 19:39:28 +00:00
public class SparkleEventArgs : System.EventArgs {
2010-07-24 14:03:58 +00:00
public string Type;
2010-07-21 19:39:28 +00:00
public string Message;
2010-07-24 14:03:58 +00:00
public SparkleEventArgs (string type)
2010-07-21 19:39:28 +00:00
{
2010-07-24 21:31:24 +00:00
2010-07-24 14:03:58 +00:00
Type = type;
2010-07-24 21:31:24 +00:00
2010-07-21 19:39:28 +00:00
}
2010-07-21 19:39:28 +00:00
}
2010-07-24 21:31:24 +00:00
// Arguments for the NewCommit event
public class NewCommitArgs : System.EventArgs {
public string Author;
public string Email;
public string Message;
public NewCommitArgs (string author, string email, string message)
{
2010-07-24 21:31:24 +00:00
Author = author;
Email = email;
Message = message;
2010-07-24 21:31:24 +00:00
}
}
}