diff options
author | Iain Lane <laney@debian.org> | 2011-06-15 16:22:22 +0100 |
---|---|---|
committer | Iain Lane <laney@debian.org> | 2011-06-15 16:22:22 +0100 |
commit | 040fed94f0bcc4e9966ab31cc68d31fa48c27ee3 (patch) | |
tree | 808bd5ff78033ca1c23b8df80f55dc544aed98b7 /SparkleLib |
Imported Upstream version 0.2.1
Diffstat (limited to 'SparkleLib')
-rw-r--r-- | SparkleLib/AssemblyInfo.cs | 11 | ||||
-rw-r--r-- | SparkleLib/AssemblyInfo.cs.in | 11 | ||||
-rw-r--r-- | SparkleLib/Defines.cs | 30 | ||||
-rw-r--r-- | SparkleLib/Defines.cs.in | 30 | ||||
-rw-r--r-- | SparkleLib/Git/SparkleFetcherGit.cs | 195 | ||||
-rw-r--r-- | SparkleLib/Git/SparkleRepoGit.cs | 556 | ||||
-rw-r--r-- | SparkleLib/Hg/SparkleFetcherHg.cs | 172 | ||||
-rw-r--r-- | SparkleLib/Hg/SparkleRepoHg.cs | 319 | ||||
-rw-r--r-- | SparkleLib/Makefile.am | 41 | ||||
-rw-r--r-- | SparkleLib/Makefile.in | 606 | ||||
-rw-r--r-- | SparkleLib/Scp/SparkleFetcherScp.cs | 139 | ||||
-rw-r--r-- | SparkleLib/Scp/SparkleRepoScp.cs | 115 | ||||
-rw-r--r-- | SparkleLib/SparkleBackend.cs | 125 | ||||
-rw-r--r-- | SparkleLib/SparkleChangeSet.cs | 37 | ||||
-rw-r--r-- | SparkleLib/SparkleConfig.cs | 258 | ||||
-rw-r--r-- | SparkleLib/SparkleFetcherBase.cs | 199 | ||||
-rw-r--r-- | SparkleLib/SparkleHelpers.cs | 80 | ||||
-rw-r--r-- | SparkleLib/SparkleListenerBase.cs | 201 | ||||
-rw-r--r-- | SparkleLib/SparkleListenerIrc.cs | 160 | ||||
-rw-r--r-- | SparkleLib/SparkleOptions.cs | 1101 | ||||
-rw-r--r-- | SparkleLib/SparklePaths.cs | 36 | ||||
-rw-r--r-- | SparkleLib/SparkleRepoBase.cs | 468 |
22 files changed, 4890 insertions, 0 deletions
diff --git a/SparkleLib/AssemblyInfo.cs b/SparkleLib/AssemblyInfo.cs new file mode 100644 index 0000000..ebfcf8f --- /dev/null +++ b/SparkleLib/AssemblyInfo.cs @@ -0,0 +1,11 @@ +/* + * AssemblyInfo.cs + * + * This is free software. See COPYING for details. + */ + +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyVersion("0.2.1")] +[assembly: AssemblyTitle ("SparkleShare")] diff --git a/SparkleLib/AssemblyInfo.cs.in b/SparkleLib/AssemblyInfo.cs.in new file mode 100644 index 0000000..11ce20d --- /dev/null +++ b/SparkleLib/AssemblyInfo.cs.in @@ -0,0 +1,11 @@ +/* + * AssemblyInfo.cs + * + * This is free software. See COPYING for details. + */ + +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyVersion("@ASM_VERSION@")] +[assembly: AssemblyTitle ("SparkleShare")] diff --git a/SparkleLib/Defines.cs b/SparkleLib/Defines.cs new file mode 100644 index 0000000..6ffd15f --- /dev/null +++ b/SparkleLib/Defines.cs @@ -0,0 +1,30 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +using System; + +namespace SparkleLib { + + public class Defines { + + public const string VERSION = "0.2.1"; + public const string LOCALE_DIR = "/usr/share/locale"; + public const string DATAROOTDIR = "/usr/share"; + public const string GETTEXT_PACKAGE = "sparkleshare"; + public const string PREFIX = "/usr"; + } +} + diff --git a/SparkleLib/Defines.cs.in b/SparkleLib/Defines.cs.in new file mode 100644 index 0000000..bee485a --- /dev/null +++ b/SparkleLib/Defines.cs.in @@ -0,0 +1,30 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +using System; + +namespace SparkleLib { + + public class Defines { + + public const string VERSION = "@VERSION@"; + public const string LOCALE_DIR = "@prefix@/share/locale"; + public const string DATAROOTDIR = "@expanded_datadir@"; + public const string GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@"; + public const string PREFIX = "@prefix@"; + } +} + diff --git a/SparkleLib/Git/SparkleFetcherGit.cs b/SparkleLib/Git/SparkleFetcherGit.cs new file mode 100644 index 0000000..49ada7a --- /dev/null +++ b/SparkleLib/Git/SparkleFetcherGit.cs @@ -0,0 +1,195 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.IO; +using System.Diagnostics; +using System.Xml; + +namespace SparkleLib { + + // Sets up a fetcher that can get remote folders + public class SparkleFetcherGit : SparkleFetcherBase { + + public SparkleFetcherGit (string server, string remote_folder, string target_folder) : + base (server, remote_folder, target_folder) + { + remote_folder = remote_folder.Trim ("/".ToCharArray ()); + + // Gitorious formatting + if (server.Contains ("gitorious.org")) { + server = "ssh://git@gitorious.org"; + + if (!remote_folder.EndsWith (".git")) { + + if (!remote_folder.Contains ("/")) + remote_folder = remote_folder + "/" + remote_folder; + + remote_folder += ".git"; + } + + } else if (server.Contains ("github.com")) { + server = "ssh://git@github.com"; + + } else if (server.Contains ("gnome.org")) { + server = "ssh://git@gnome.org/git"; + + } else { + server = server.TrimEnd ("/".ToCharArray ()); + + if (server.StartsWith ("ssh://")) + server = server.Substring (6); + + if (!server.Contains ("@")) + server = "git@" + server; + + server = "ssh://" + server; + } + + base.target_folder = target_folder; + base.remote_url = server + "/" + remote_folder; + } + + + public override bool Fetch () + { + SparkleGit git = new SparkleGit (SparklePaths.SparkleTmpPath, + "clone \"" + base.remote_url + "\" " + "\"" + base.target_folder + "\""); + + git.Start (); + git.WaitForExit (); + + SparkleHelpers.DebugInfo ("Git", "Exit code " + git.ExitCode.ToString ()); + + if (git.ExitCode != 0) { + return false; + } else { + InstallConfiguration (); + InstallExcludeRules (); + return true; + } + } + + + // Install the user's name and email and some config into + // the newly cloned repository + private void InstallConfiguration () + { + string global_config_file_path = Path.Combine (SparklePaths.SparkleConfigPath, "config.xml"); + + if (!File.Exists (global_config_file_path)) + return; + + string repo_config_file_path = SparkleHelpers.CombineMore (base.target_folder, ".git", "config"); + string config = String.Join (Environment.NewLine, File.ReadAllLines (repo_config_file_path)); + + // Be case sensitive explicitly to work on Mac + config = config.Replace ("ignorecase = true", "ignorecase = false"); + + // Ignore permission changes + config = config.Replace ("filemode = true", "filemode = false"); + + // Add user info + string n = Environment.NewLine; + XmlDocument xml = new XmlDocument(); + xml.Load (global_config_file_path); + + XmlNode node_name = xml.SelectSingleNode ("//user/name/text()"); + XmlNode node_email = xml.SelectSingleNode ("//user/email/text()"); + + // TODO: just use commands instead of messing with the config file + config += n + + "[user]" + n + + "\tname = " + node_name.Value + n + + "\temail = " + node_email.Value + n; + + // Write the config to the file + TextWriter writer = new StreamWriter (repo_config_file_path); + writer.WriteLine (config); + writer.Close (); + + SparkleHelpers.DebugInfo ("Config", "Added configuration to '" + repo_config_file_path + "'"); + } + + + // Add a .gitignore file to the repo + private void InstallExcludeRules () + { + string exlude_rules_file_path = SparkleHelpers.CombineMore ( + this.target_folder, ".git", "info", "exclude"); + + TextWriter writer = new StreamWriter (exlude_rules_file_path); + + // gedit and emacs + writer.WriteLine ("*~"); + + // vi(m) + writer.WriteLine (".*.sw[a-z]"); + writer.WriteLine ("*.un~"); + writer.WriteLine ("*.swp"); + writer.WriteLine ("*.swo"); + + // KDE + writer.WriteLine (".directory"); + + // Mac OSX + writer.WriteLine (".DS_Store"); + writer.WriteLine ("Icon?"); + writer.WriteLine ("._*"); + writer.WriteLine (".Spotlight-V100"); + writer.WriteLine (".Trashes"); + + // Mac OSX + writer.WriteLine ("*(Autosaved).graffle"); + + // Windows + writer.WriteLine ("Thumbs.db"); + writer.WriteLine ("Desktop.ini"); + + // CVS + writer.WriteLine ("*/CVS/*"); + writer.WriteLine (".cvsignore"); + writer.WriteLine ("*/.cvsignore"); + + // Subversion + writer.WriteLine ("/.svn/*"); + writer.WriteLine ("*/.svn/*"); + + writer.Close (); + } + } + + public class SparkleGit : Process { + + public SparkleGit (string path, string args) : base () + { + EnableRaisingEvents = true; + StartInfo.FileName = SparkleBackend.DefaultBackend.Path; + StartInfo.Arguments = args; + StartInfo.RedirectStandardOutput = true; + StartInfo.UseShellExecute = false; + StartInfo.WorkingDirectory = path; + } + + + new public void Start () + { + SparkleHelpers.DebugInfo ("Cmd", StartInfo.FileName + " " + StartInfo.Arguments); + base.Start (); + } + } +} diff --git a/SparkleLib/Git/SparkleRepoGit.cs b/SparkleLib/Git/SparkleRepoGit.cs new file mode 100644 index 0000000..3a611ca --- /dev/null +++ b/SparkleLib/Git/SparkleRepoGit.cs @@ -0,0 +1,556 @@ +// 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 +// 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; + +namespace SparkleLib { + + public class SparkleRepoGit : SparkleRepoBase { + + public SparkleRepoGit (string path, SparkleBackend backend) : + base (path, backend) { } + + + public override 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, "rev-list --reverse HEAD"); + 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 (); + git.WaitForExit (); + + return output.Substring (0, 40); + } + } + + + 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"); + if (File.Exists (rebase_apply_file)) + File.Delete (rebase_apply_file); + + SparkleGit git = new SparkleGit (LocalPath, "log -1 --format=%H"); + git.Start (); + git.WaitForExit (); + + if (git.ExitCode == 0) { + string output = git.StandardOutput.ReadToEnd (); + return output.TrimEnd (); + } else { + return null; + } + } + } + + + public override bool CheckForRemoteChanges () + { + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Checking for remote changes..."); + SparkleGit git = new SparkleGit (LocalPath, "ls-remote origin master"); + + git.Start (); + git.WaitForExit (); + + if (git.ExitCode != 0) + return false; + + string remote_revision = git.StandardOutput.ReadToEnd ().TrimEnd (); + + if (!remote_revision.StartsWith (CurrentRevision)) { + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Remote changes found. (" + remote_revision + ")"); + return true; + } else { + return false; + } + } + + + public override bool SyncUp () + { + Add (); + + string message = FormatCommitMessage (); + Commit (message); + + SparkleGit git = new SparkleGit (LocalPath, "push origin master"); + + git.Start (); + git.WaitForExit (); + + if (git.ExitCode == 0) + return true; + else + return false; + } + + + public override bool SyncDown () + { + SparkleGit git = new SparkleGit (LocalPath, "fetch -v origin master"); + + git.Start (); + git.WaitForExit (); + + if (git.ExitCode == 0) { + Rebase (); + return true; + } else { + return false; + } + } + + + public override 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; + } + } + + + public override bool HasUnsyncedChanges { + get { + string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath, + ".git", "has_unsynced_changes"); + + return File.Exists (unsynced_file_path); + } + + 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); + } else { + File.Delete (unsynced_file_path); + } + } + } + + + // Stages the made changes + private void Add () + { + SparkleGit git = new SparkleGit (LocalPath, "add --all"); + git.Start (); + git.WaitForExit (); + + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes staged"); + } + + + // Removes unneeded objects + private void CollectGarbage () + { + SparkleGit git = new SparkleGit (LocalPath, "gc"); + git.Start (); + git.WaitForExit (); + + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Garbage collected."); + } + + + // Commits the made changes + private void Commit (string message) + { + if (!AnyDifferences) + return; + + SparkleGit git = new SparkleGit (LocalPath, "commit -m '" + message + "'"); + git.Start (); + git.WaitForExit (); + + SparkleHelpers.DebugInfo ("Commit", "[" + Name + "] " + message); + + // Collect garbage pseudo-randomly. Turn off for + // now: too resource heavy. + // if (DateTime.Now.Second % 10 == 0) + // CollectGarbage (); + } + + + // Merges the fetched changes + private void Rebase () + { + DisableWatching (); + + if (AnyDifferences) { + Add (); + + string commit_message = FormatCommitMessage (); + Commit (commit_message); + } + + SparkleGit git = new SparkleGit (LocalPath, "rebase -v FETCH_HEAD"); + + git.Start (); + git.WaitForExit (); + + if (git.ExitCode != 0) { + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Conflict detected. Trying to get out..."); + + while (AnyDifferences) + ResolveConflict (); + + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Conflict resolved."); + + OnConflictResolved (); + } + + EnableWatching (); + } + + + 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 + " (" + SparkleConfig.DefaultConfig.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 (); + } + } + } + + + // Returns a list of the latest change sets + // TODO: Method needs to be made a lot faster + public override List <SparkleChangeSet> GetChangeSets (int count) + { + if (count < 1) + count = 30; + + List <SparkleChangeSet> change_sets = new List <SparkleChangeSet> (); + + 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) { + SparkleChangeSet change_set = new SparkleChangeSet (); + + change_set.Revision = match.Groups [1].Value; + change_set.UserName = match.Groups [2].Value; + change_set.UserEmail = match.Groups [3].Value; + change_set.IsMerge = is_merge_commit; + + + change_set.Timestamp = new DateTime (int.Parse (match.Groups [4].Value), + int.Parse (match.Groups [5].Value), int.Parse (match.Groups [6].Value), + int.Parse (match.Groups [7].Value), int.Parse (match.Groups [8].Value), + int.Parse (match.Groups [9].Value)); + + + string time_zone = match.Groups [10].Value; + int our_offset = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now).Hours; + int their_offset = int.Parse (time_zone.Substring (1, 2)); + + // Convert their timestamp to UTC timezone + if (their_offset > 0) + change_set.Timestamp = change_set.Timestamp.AddHours (their_offset * -1); + else + change_set.Timestamp = change_set.Timestamp.AddHours (their_offset); + + // Convert the UTC timestamp into our timezone + if (our_offset > 0) + change_set.Timestamp = change_set.Timestamp.AddHours (our_offset); + else + change_set.Timestamp = change_set.Timestamp.AddHours (our_offset * -1); + + + 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")) { + change_set.Added.Add (file_path); + } else if (change_type.Equals ("M")) { + change_set.Edited.Add (file_path); + } else if (change_type.Equals ("D")) { + change_set.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); + + change_set.MovedFrom.Add (file_path); + change_set.MovedTo.Add (to_file_path); + } + } + } + + change_sets.Add (change_set); + } + } + + return change_sets; + } + + + // 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 ()); + message += "+ ‘" + file_name + "’" + n; + + count++; + if (count == max_count) + return message + "..."; + } + + foreach (string modified in Modified) { + file_name = modified.Trim ("\"".ToCharArray ()); + message += "/ ‘" + file_name + "’" + n; + + count++; + if (count == max_count) + return message + "..."; + } + + foreach (string removed in Removed) { + file_name = removed.Trim ("\"".ToCharArray ()); + message += "- ‘" + file_name + "’" + n; + + count++; + if (count == max_count) + return message + "..." + n; + } + + return message.TrimEnd (); + } + + + public override void CreateInitialChangeSet () + { + base.CreateInitialChangeSet (); + Add (); + + string message = FormatCommitMessage (); + Commit (message); + } + + + public override bool UsesNotificationCenter + { + get { + string file_path = SparkleHelpers.CombineMore (LocalPath, ".git", "disable_notification_center"); + return !File.Exists (file_path); + } + } + } +} diff --git a/SparkleLib/Hg/SparkleFetcherHg.cs b/SparkleLib/Hg/SparkleFetcherHg.cs new file mode 100644 index 0000000..d89a324 --- /dev/null +++ b/SparkleLib/Hg/SparkleFetcherHg.cs @@ -0,0 +1,172 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.IO; +using System.Diagnostics; +using System.Xml; + +namespace SparkleLib { + + // Sets up a fetcher that can get remote folders + public class SparkleFetcherHg : SparkleFetcherBase { + + public SparkleFetcherHg (string server, string remote_folder, string target_folder) : + base (server, remote_folder, target_folder) { } + + + public override bool Fetch () + { + SparkleHg hg = new SparkleHg (SparklePaths.SparkleTmpPath, + "clone \"" + base.remote_url + "\" " + "\"" + base.target_folder + "\""); + + hg.Start (); + hg.WaitForExit (); + + SparkleHelpers.DebugInfo ("Hg", "Exit code " + hg.ExitCode.ToString ()); + + if (hg.ExitCode != 0) { + return false; + } else { + InstallConfiguration (); + InstallExcludeRules (); + return true; + } + } + + + // Install the user's name and email and some config into + // the newly cloned repository + private void InstallConfiguration () + { + string global_config_file_path = Path.Combine (SparklePaths.SparkleConfigPath, "config.xml"); + + if (!File.Exists (global_config_file_path)) + return; + + string repo_config_file_path = SparkleHelpers.CombineMore (base.target_folder, ".hg", "hgrc"); + string config = String.Join (Environment.NewLine, File.ReadAllLines (repo_config_file_path)); + + // Add user info + string n = Environment.NewLine; + XmlDocument xml = new XmlDocument(); + xml.Load (global_config_file_path); + + XmlNode node_name = xml.SelectSingleNode ("//user/name/text()"); + XmlNode node_email = xml.SelectSingleNode ("//user/email/text()"); + + // TODO this ignore duplicate names (FolderName (2)) + string ignore_file_path = base.target_folder.Replace (SparklePaths.SparkleTmpPath, + SparklePaths.SparklePath); + + ignore_file_path = SparkleHelpers.CombineMore (ignore_file_path, ".hg", "hgignore"); + + config += n + + "[ui]" + n + + "username = " + node_name.Value + " <" + node_email.Value + ">" + n + + "ignore = " + ignore_file_path + n; + + // Write the config to the file + TextWriter writer = new StreamWriter (repo_config_file_path); + writer.WriteLine (config); + writer.Close (); + + string style_file_path = SparkleHelpers.CombineMore (base.target_folder, ".hg", "log.style"); + + string style = "changeset = \"{file_mods}{file_adds}{file_dels}\"" + n + + "file_add = \"A {file_add}\\n\"" + n + + "file_mod = \"M {file_mod}\\n\"" + n + + "file_del = \"D {file_del}\\n\"" + n; + + writer = new StreamWriter (style_file_path); + writer.WriteLine (style); + writer.Close (); + + SparkleHelpers.DebugInfo ("Config", "Added configuration to '" + repo_config_file_path + "'"); + } + + + // Add a .gitignore file to the repo + private void InstallExcludeRules () + { + string exlude_rules_file_path = SparkleHelpers.CombineMore ( + this.target_folder, ".hg", "hgignore"); + + TextWriter writer = new StreamWriter (exlude_rules_file_path); + + writer.WriteLine ("syntax: glob"); + + // gedit and emacs + writer.WriteLine ("*~"); + + // vi(m) + writer.WriteLine (".*.sw[a-z]"); + writer.WriteLine ("*.un~"); + writer.WriteLine ("*.swp"); + writer.WriteLine ("*.swo"); + + // KDE + writer.WriteLine (".directory"); + + // Mac OSX + writer.WriteLine (".DS_Store"); + writer.WriteLine ("Icon?"); + writer.WriteLine ("._*"); + writer.WriteLine (".Spotlight-V100"); + writer.WriteLine (".Trashes"); + + // Mac OSX + writer.WriteLine ("*(Autosaved).graffle"); + + // Windows + writer.WriteLine ("Thumbs.db"); + writer.WriteLine ("Desktop.ini"); + + // CVS + writer.WriteLine ("*/CVS/*"); + writer.WriteLine (".cvsignore"); + writer.WriteLine ("*/.cvsignore"); + + // Subversion + writer.WriteLine ("/.svn/*"); + writer.WriteLine ("*/.svn/*"); + + writer.Close (); + } + } + + + public class SparkleHg : Process { + + public SparkleHg (string path, string args) : base () + { + EnableRaisingEvents = true; + StartInfo.FileName = "/opt/local/bin/hg"; + StartInfo.Arguments = args; + StartInfo.RedirectStandardOutput = true; + StartInfo.UseShellExecute = false; + StartInfo.WorkingDirectory = path; + } + + + new public void Start () + { + SparkleHelpers.DebugInfo ("Cmd", StartInfo.FileName + " " + StartInfo.Arguments); + base.Start (); + } + } +} diff --git a/SparkleLib/Hg/SparkleRepoHg.cs b/SparkleLib/Hg/SparkleRepoHg.cs new file mode 100644 index 0000000..149c9f8 --- /dev/null +++ b/SparkleLib/Hg/SparkleRepoHg.cs @@ -0,0 +1,319 @@ +// 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 +// 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; + +namespace SparkleLib { + + public class SparkleRepoHg : SparkleRepoBase { + + public SparkleRepoHg (string path, SparkleBackend backend) : + base (path, backend) { } + + + public override string Identifier { + get { + SparkleHg hg = new SparkleHg (LocalPath, "log -r : --limit 1 --template \"{node}\""); + hg.Start (); + hg.WaitForExit (); + + return hg.StandardOutput.ReadToEnd (); + } + } + + + public override string CurrentRevision { + get { + SparkleHg hg = new SparkleHg (LocalPath, "log --limit 1 --template \"{node}\""); + hg.Start (); + hg.WaitForExit (); + + string hash = hg.StandardOutput.ReadToEnd ().Trim (); + if (hash.Length > 0) + return hash; + else + return null; + } + } + + + public override bool CheckForRemoteChanges () + { + return true; // Mercurial doesn't have a way to check for the remote hash + } + + + public override bool SyncUp () + { + Add (); + + string message = FormatCommitMessage (); + Commit (message); + + SparkleHg hg = new SparkleHg (LocalPath, "push"); + + hg.Start (); + hg.WaitForExit (); + + if (hg.ExitCode == 0) { + return true; + } else { + return false; + } + } + + + public override bool SyncDown () + { + SparkleHg hg = new SparkleHg (LocalPath, "pull"); + + hg.Start (); + hg.WaitForExit (); + + if (hg.ExitCode == 0) { + Merge (); + return true; + } else { + return false; + } + } + + + public override bool AnyDifferences { + get { + SparkleHg hg = new SparkleHg (LocalPath, "status"); + hg.Start (); + hg.WaitForExit (); + + string output = hg.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; + } + } + + + public override bool HasUnsyncedChanges { + get { + string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath, + ".hg", "has_unsynced_changes"); + + return File.Exists (unsynced_file_path); + } + + set { + string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath, + ".hg", "has_unsynced_changes"); + + if (value) { + if (!File.Exists (unsynced_file_path)) + File.Create (unsynced_file_path); + } else { + File.Delete (unsynced_file_path); + } + } + } + + + // Stages the made changes + private void Add () + { + SparkleHg hg = new SparkleHg (LocalPath, "addremove --quiet"); + hg.Start (); + hg.WaitForExit (); + + SparkleHelpers.DebugInfo ("Hg", "[" + Name + "] Changes staged"); + } + + + // Commits the made changes + private void Commit (string message) + { + if (!AnyDifferences) + return; + + SparkleHg hg = new SparkleHg (LocalPath, "commit -m '" + message + "'"); + hg.Start (); + hg.WaitForExit (); + + SparkleHelpers.DebugInfo ("Commit", "[" + Name + "] " + message); + } + + + // Merges the fetched changes + private void Merge () + { + DisableWatching (); + + if (AnyDifferences) { + Add (); + + string commit_message = FormatCommitMessage (); + Commit (commit_message); + } + + SparkleHg hg = new SparkleHg (LocalPath, "update"); + + hg.Start (); + hg.WaitForExit (); + + EnableWatching (); + } + + + // Returns a list of the latest change sets + // TODO: Method needs to be made a lot faster + public override List<SparkleChangeSet> GetChangeSets (int count) + { + if (count < 1) + count = 30; + + List <SparkleChangeSet> change_sets = new List <SparkleChangeSet> (); + + SparkleHg hg_log = new SparkleHg (LocalPath, "log --limit " + count + " --style changelog --verbose --stat"); + Console.OutputEncoding = System.Text.Encoding.Unicode; + hg_log.Start (); + + // Reading the standard output HAS to go before + // WaitForExit, or it will hang forever on output > 4096 bytes + string output = hg_log.StandardOutput.ReadToEnd (); + hg_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 ("2") && line.EndsWith (")") && j > 0) { + entries.Add (entry); + entry = ""; + } + + entry += line + "\n"; + j++; + + last_entry = entry; + } + + entries.Add (last_entry); + + Regex regex = new Regex (@"([0-9]{4})-([0-9]{2})-([0-9]{2}).*([0-9]{2}):([0-9]{2}).*.([0-9]{4})" + + "(.+)<(.+)>.*.([a-z0-9]{12})", RegexOptions.Compiled); + + // TODO: Need to optimise for speed + foreach (string log_entry in entries) { + + bool is_merge_commit = false; + + Match match = regex.Match (log_entry); + + if (!match.Success) + continue; + + SparkleChangeSet change_set = new SparkleChangeSet () { + Revision = match.Groups [9].Value, + UserName = match.Groups [7].Value.Trim (), + UserEmail = match.Groups [8].Value, + IsMerge = is_merge_commit + }; + + change_set.Timestamp = new DateTime (int.Parse (match.Groups [1].Value), + int.Parse (match.Groups [2].Value), int.Parse (match.Groups [3].Value), + int.Parse (match.Groups [4].Value), int.Parse (match.Groups [5].Value), 0); + + string [] entry_lines = log_entry.Split ("\n".ToCharArray ()); + + foreach (string entry_line in entry_lines) { + if (!entry_line.StartsWith ("\t* ")) + continue; + + if (entry_line.EndsWith ("new file.")) { + string files = entry_line.Substring (3, entry_line.Length - 13); + string [] added_files = files.Split (",".ToCharArray ()); + + foreach (string added_file in added_files) { + string file = added_file.TrimEnd (": ".ToCharArray ()); + change_set.Added.Add (file); + } + + } else if (entry_line.EndsWith ("deleted file.")) { + string files = entry_line.Substring (3, entry_line.Length - 17); + string [] deleted_files = files.Split (",".ToCharArray ()); + + foreach (string deleted_file in deleted_files) { + string file = deleted_file.TrimEnd (": ".ToCharArray ()); + change_set.Deleted.Add (file); + } + + } else if (!"".Equals (entry_line.Trim ())){ + string files = entry_line.Substring (3); + files = files.TrimEnd (":".ToCharArray()); + string [] edited_files = files.Split (",".ToCharArray ()); + + foreach (string edited_file in edited_files) { + if (!change_set.Added.Contains (edited_file) && + !change_set.Deleted.Contains (edited_file)) { + + change_set.Edited.Add (edited_file); + } + } + } + } + + change_sets.Add (change_set); + } + + return change_sets; + } + + + // Creates a pretty commit message based on what has changed + private string FormatCommitMessage () // TODO + { + return "SparkleShare Hg"; + } + + + public override void CreateInitialChangeSet () + { + base.CreateInitialChangeSet (); + Add (); + + string message = FormatCommitMessage (); + Commit (message); + } + + + public override bool UsesNotificationCenter + { + get { + string file_path = SparkleHelpers.CombineMore (LocalPath, ".hg", "disable_notification_center"); + return !File.Exists (file_path); + } + } + } +} diff --git a/SparkleLib/Makefile.am b/SparkleLib/Makefile.am new file mode 100644 index 0000000..b29fa92 --- /dev/null +++ b/SparkleLib/Makefile.am @@ -0,0 +1,41 @@ +ASSEMBLY = SparkleLib +TARGET = library + +LINK = $(REF_SPARKLELIB) + + +SOURCES = \ + Defines.cs \ + Git/SparkleFetcherGit.cs \ + Git/SparkleRepoGit.cs \ + Hg/SparkleFetcherHg.cs \ + Hg/SparkleRepoHg.cs \ + Scp/SparkleFetcherScp.cs \ + Scp/SparkleRepoScp.cs \ + SparkleBackend.cs \ + SparkleChangeSet.cs \ + SparkleConfig.cs \ + SparkleFetcherBase.cs \ + SparkleHelpers.cs \ + SparkleListenerBase.cs \ + SparkleListenerIrc.cs \ + SparkleOptions.cs \ + SparklePaths.cs \ + SparkleRepoBase.cs + + +SMARTIRC4NET_FILES_EXPANDED = $(foreach file, $(SMARTIRC4NET_FILES), $(top_builddir)/$(file)) + +EXTRA_BUNDLE = $(SMARTIRC4NET_FILES_EXPANDED) + +install-data-hook: + for ASM in $(EXTRA_BUNDLE); do \ + $(INSTALL) -m 0755 $$ASM $(DESTDIR)$(moduledir); \ + done; + +uninstall-hook: + for ASM in $(EXTRA_BUNDLE); do \ + rm -f $(DESTDIR)$(moduledir)/`basename $$ASM`; \ + done; + +include $(top_srcdir)/build/build.mk diff --git a/SparkleLib/Makefile.in b/SparkleLib/Makefile.in new file mode 100644 index 0000000..52d6c7d --- /dev/null +++ b/SparkleLib/Makefile.in @@ -0,0 +1,606 @@ +# Makefile.in generated by automake 1.11.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, +# 2003, 2004, 2005, 2006, 2007, 2008, 2009 Free Software Foundation, +# Inc. +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + +VPATH = @srcdir@ +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +DIST_COMMON = $(srcdir)/AssemblyInfo.cs.in $(srcdir)/Defines.cs.in \ + $(srcdir)/Makefile.am $(srcdir)/Makefile.in \ + $(top_srcdir)/build/build.environment.mk \ + $(top_srcdir)/build/build.mk \ + $(top_srcdir)/build/build.rules.mk +@ENABLE_TESTS_TRUE@am__append_1 = " $(NUNIT_LIBS)" +subdir = SparkleLib +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/build/m4/gnome-doc-utils.m4 \ + $(top_srcdir)/build/m4/shave/shave.m4 \ + $(top_srcdir)/build/m4/shamrock/expansions.m4 \ + $(top_srcdir)/build/m4/shamrock/gnome-doc.m4 \ + $(top_srcdir)/build/m4/shamrock/mono.m4 \ + $(top_srcdir)/build/m4/shamrock/nunit.m4 \ + $(top_srcdir)/build/m4/shamrock/programs.m4 \ + $(top_srcdir)/build/m4/sparkleshare/gtk-sharp.m4 \ + $(top_srcdir)/build/m4/sparkleshare/nautilus-python.m4 \ + $(top_srcdir)/build/m4/sparkleshare/notify-sharp.m4 \ + $(top_srcdir)/build/m4/sparkleshare/smartirc4net.m4 \ + $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(install_sh) -d +CONFIG_CLEAN_FILES = AssemblyInfo.cs Defines.cs +CONFIG_CLEAN_VPATH_FILES = +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__installdirs = "$(DESTDIR)$(moduledir)" +SCRIPTS = $(module_SCRIPTS) +DIST_SOURCES = +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@ +ALL_LINGUAS = @ALL_LINGUAS@ +AMTAR = @AMTAR@ +AM_MAKEFLAGS = @AM_MAKEFLAGS@ +ASM_VERSION = @ASM_VERSION@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CFLAGS = @CFLAGS@ +CPPFLAGS = @CPPFLAGS@ +CXX = @CXX@ +CYGPATH_W = @CYGPATH_W@ +DATADIRNAME = @DATADIRNAME@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EXEEXT = @EXEEXT@ +F77 = @F77@ +FC = @FC@ +GETTEXT_PACKAGE = @GETTEXT_PACKAGE@ +GLIBSHARP_2_12_7_CFLAGS = @GLIBSHARP_2_12_7_CFLAGS@ +GLIBSHARP_2_12_7_LIBS = @GLIBSHARP_2_12_7_LIBS@ +GLIBSHARP_CFLAGS = @GLIBSHARP_CFLAGS@ +GLIBSHARP_LIBS = @GLIBSHARP_LIBS@ +GMSGFMT = @GMSGFMT@ +GNOME_DOC_UTILS_CFLAGS = @GNOME_DOC_UTILS_CFLAGS@ +GNOME_DOC_UTILS_LIBS = @GNOME_DOC_UTILS_LIBS@ +GTKSHARP_A11Y_CFLAGS = @GTKSHARP_A11Y_CFLAGS@ +GTKSHARP_A11Y_LIBS = @GTKSHARP_A11Y_LIBS@ +GTKSHARP_CFLAGS = @GTKSHARP_CFLAGS@ +GTKSHARP_LIBS = @GTKSHARP_LIBS@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@ +INTLTOOL_MERGE = @INTLTOOL_MERGE@ +INTLTOOL_PERL = @INTLTOOL_PERL@ +INTLTOOL_UPDATE = @INTLTOOL_UPDATE@ +LDFLAGS = @LDFLAGS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LIBTOOL = @LIBTOOL@ +LTLIBOBJS = @LTLIBOBJS@ +MAINT = @MAINT@ +MAKEFLAGS = @MAKEFLAGS@ +MAKEINFO = @MAKEINFO@ +MCS = @MCS@ +MKDIR_P = @MKDIR_P@ +MONO = @MONO@ +MONO_MODULE_CFLAGS = @MONO_MODULE_CFLAGS@ +MONO_MODULE_LIBS = @MONO_MODULE_LIBS@ +MSGFMT = @MSGFMT@ +MSGMERGE = @MSGMERGE@ +NAUTILUS_PREFIX = @NAUTILUS_PREFIX@ +NAUTILUS_PYTHON_CFLAGS = @NAUTILUS_PYTHON_CFLAGS@ +NAUTILUS_PYTHON_DIR = @NAUTILUS_PYTHON_DIR@ +NAUTILUS_PYTHON_LIBS = @NAUTILUS_PYTHON_LIBS@ +NOTIFY_SHARP_CFLAGS = @NOTIFY_SHARP_CFLAGS@ +NOTIFY_SHARP_LIBS = @NOTIFY_SHARP_LIBS@ +NUNIT_CFLAGS = @NUNIT_CFLAGS@ +NUNIT_LIBS = @NUNIT_LIBS@ +OBJC = @OBJC@ +OBJEXT = @OBJEXT@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CONFIG = @PKG_CONFIG@ +PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ +PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +Q = @Q@ +SED = @SED@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +SMARTIRC4NET_ASSEMBLY = @SMARTIRC4NET_ASSEMBLY@ +SMARTIRC4NET_FILES = @SMARTIRC4NET_FILES@ +STRIP = @STRIP@ +USE_NLS = @USE_NLS@ +V = @V@ +VERSION = @VERSION@ +WEBKIT_SHARP_CFLAGS = @WEBKIT_SHARP_CFLAGS@ +WEBKIT_SHARP_LIBS = @WEBKIT_SHARP_LIBS@ +XGETTEXT = @XGETTEXT@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_CC = @ac_ct_CC@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build_alias = @build_alias@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +expanded_bindir = @expanded_bindir@ +expanded_datadir = @expanded_datadir@ +expanded_libdir = @expanded_libdir@ +host_alias = @host_alias@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +shavedir = @shavedir@ +srcdir = @srcdir@ +subdirs = @subdirs@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +ASSEMBLY = SparkleLib +TARGET = library +LINK = $(REF_SPARKLELIB) $(am__append_1) +SOURCES = \ + Defines.cs \ + Git/SparkleFetcherGit.cs \ + Git/SparkleRepoGit.cs \ + Hg/SparkleFetcherHg.cs \ + Hg/SparkleRepoHg.cs \ + Scp/SparkleFetcherScp.cs \ + Scp/SparkleRepoScp.cs \ + SparkleBackend.cs \ + SparkleChangeSet.cs \ + SparkleConfig.cs \ + SparkleFetcherBase.cs \ + SparkleHelpers.cs \ + SparkleListenerBase.cs \ + SparkleListenerIrc.cs \ + SparkleOptions.cs \ + SparklePaths.cs \ + SparkleRepoBase.cs + +SMARTIRC4NET_FILES_EXPANDED = $(foreach file, $(SMARTIRC4NET_FILES), $(top_builddir)/$(file)) +EXTRA_BUNDLE = $(SMARTIRC4NET_FILES_EXPANDED) + +# Initializers +MONO_BASE_PATH = + +# Install Paths +DEFAULT_INSTALL_DIR = $(pkglibdir) +DIR_BIN = $(top_builddir)/bin + +# External libraries to link against, generated from configure +LINK_SYSTEM = -r:System +LINK_MONO_POSIX = -r:Mono.Posix +LINK_GLIB = $(GLIBSHARP_LIBS) +LINK_GTK = $(GTKSHARP_LIBS) +LINK_GNOME = $(GNOME_SHARP_LIBS) +LINK_DBUS = $(NDESK_DBUS_LIBS) $(NDESK_DBUS_GLIB_LIBS) +LINK_DBUS_NO_GLIB = $(NDESK_DBUS_LIBS) +LINK_SMARTIRC4NET = -r:$(top_builddir)/$(SMARTIRC4NET_ASSEMBLY) +REF_NOTIFY_SHARP = $(LINK_SYSTEM) $(LINK_DBUS) $(GTKSHARP_LIBS) $(GLIBSHARP_LIBS) +LINK_NOTIFY_SHARP = -r:$(DIR_BIN)/NotifySharp.dll +LINK_NOTIFY_SHARP_DEPS = $(REF_NOTIFY_SHARP) $(LINK_NOTIFY_SHARP) +REF_FRIENDFACE = $(LINK_SYSTEM) $(LINK_GTK) $(LINK_MONO_POSIX) +LINK_FRIENDFACE = -r:$(DIR_BIN)/FriendFace.dll +LINK_FRIENDFACE_DEPS = $(REF_FRIENDFACE) $(LINK_FRIENDFACE) +REF_SPARKLELIB = $(LINK_SYSTEM) $(LINK_MONO_POSIX) $(LINK_SMARTIRC4NET) +LINK_SPARKLELIB = -r:$(DIR_BIN)/SparkleLib.dll +LINK_SPARKLELIB_DEPS = $(REF_SPARKLELIB) $(LINK_SPARKLELIB) +REF_SPARKLESHARE = $(LINK_DBUS) $(LINK_GTK) $(LINK_SPARKLELIB_DEPS) +REF_SPARKLEDIFF = $(LINK_FRIENDFACE_DEPS) $(LINK_GTK) $(LINK_SPARKLELIB_DEPS) + +# Cute hack to replace a space with something +colon := : +empty := +space := $(empty) $(empty) + +# Build path to allow running uninstalled +RUN_PATH = $(subst $(space),$(colon), $(MONO_BASE_PATH)) +UNIQUE_FILTER_PIPE = tr [:space:] \\n | sort | uniq +BUILD_DATA_DIR = $(top_builddir)/bin/share/$(PACKAGE) + +# Since all other attempts failed, we currently go this way: +# This code adds the file specified in ASSEMBLY_INFO_SOURCE to SOURCES_BUILD. +# If no such file is specified, the default AssemblyInfo.cs is used. +ASSEMBLY_INFO_SOURCE_REAL = \ + $(shell if [ "$(ASSEMBLY_INFO_SOURCE)" ]; \ + then \ + echo "$(addprefix $(srcdir)/, $(ASSEMBLY_INFO_SOURCE))"; \ + else \ + echo "$(top_srcdir)/SparkleLib/AssemblyInfo.cs"; \ + fi) + +SOURCES_BUILD = $(addprefix $(srcdir)/, $(SOURCES)) \ + $(ASSEMBLY_INFO_SOURCE_REAL) +RESOURCES_EXPANDED = $(addprefix $(srcdir)/, $(RESOURCES)) +RESOURCES_BUILD = $(foreach resource, $(RESOURCES_EXPANDED), \ + -resource:$(resource),$(notdir $(resource))) + +ASSEMBLY_EXTENSION = $(strip $(patsubst library, dll, $(TARGET))) +ASSEMBLY_FILE = $(top_builddir)/bin/$(ASSEMBLY).$(ASSEMBLY_EXTENSION) +INSTALL_DIR_RESOLVED = $(firstword $(subst , $(DEFAULT_INSTALL_DIR), $(INSTALL_DIR))) +@ENABLE_TESTS_TRUE@ENABLE_TESTS_FLAG = "-define:ENABLE_TESTS" +@ENABLE_ATK_TRUE@ENABLE_ATK_FLAG = "-define:ENABLE_ATK" +FILTERED_LINK = $(shell echo "$(LINK)" | $(UNIQUE_FILTER_PIPE)) +DEP_LINK = $(shell echo "$(LINK)" | $(UNIQUE_FILTER_PIPE) | sed s,-r:,,g | grep '$(top_builddir)/bin/') +OUTPUT_FILES = \ + $(ASSEMBLY_FILE) \ + $(ASSEMBLY_FILE).mdb + +moduledir = $(INSTALL_DIR_RESOLVED) +module_SCRIPTS = $(OUTPUT_FILES) +EXTRA_DIST = $(SOURCES_BUILD) $(RESOURCES_EXPANDED) +CLEANFILES = $(OUTPUT_FILES) $(ASSEMBLY_FILE).config +DISTCLEANFILES = *.pidb +MAINTAINERCLEANFILES = Makefile.in +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(top_srcdir)/build/build.mk $(top_srcdir)/build/build.environment.mk $(top_srcdir)/build/build.rules.mk $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign SparkleLib/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign SparkleLib/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): +AssemblyInfo.cs: $(top_builddir)/config.status $(srcdir)/AssemblyInfo.cs.in + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ +Defines.cs: $(top_builddir)/config.status $(srcdir)/Defines.cs.in + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ +install-moduleSCRIPTS: $(module_SCRIPTS) + @$(NORMAL_INSTALL) + test -z "$(moduledir)" || $(MKDIR_P) "$(DESTDIR)$(moduledir)" + @list='$(module_SCRIPTS)'; test -n "$(moduledir)" || list=; \ + for p in $$list; do \ + if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ + if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \ + done | \ + sed -e 'p;s,.*/,,;n' \ + -e 'h;s|.*|.|' \ + -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \ + $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \ + { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \ + if ($$2 == $$4) { files[d] = files[d] " " $$1; \ + if (++n[d] == $(am__install_max)) { \ + print "f", d, files[d]; n[d] = 0; files[d] = "" } } \ + else { print "f", d "/" $$4, $$1 } } \ + END { for (d in files) print "f", d, files[d] }' | \ + while read type dir files; do \ + if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \ + test -z "$$files" || { \ + echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(moduledir)$$dir'"; \ + $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(moduledir)$$dir" || exit $$?; \ + } \ + ; done + +uninstall-moduleSCRIPTS: + @$(NORMAL_UNINSTALL) + @list='$(module_SCRIPTS)'; test -n "$(moduledir)" || exit 0; \ + files=`for p in $$list; do echo "$$p"; done | \ + sed -e 's,.*/,,;$(transform)'`; \ + test -n "$$list" || exit 0; \ + echo " ( cd '$(DESTDIR)$(moduledir)' && rm -f" $$files ")"; \ + cd "$(DESTDIR)$(moduledir)" && rm -f $$files + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +tags: TAGS +TAGS: + +ctags: CTAGS +CTAGS: + + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile $(SCRIPTS) +installdirs: + for dir in "$(DESTDIR)$(moduledir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + `test -z '$(STRIP)' || \ + echo "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'"` install +mostlyclean-generic: + +clean-generic: + -test -z "$(CLEANFILES)" || rm -f $(CLEANFILES) + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + -test -z "$(DISTCLEANFILES)" || rm -f $(DISTCLEANFILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." + -test -z "$(MAINTAINERCLEANFILES)" || rm -f $(MAINTAINERCLEANFILES) +clean: clean-am + +clean-am: clean-generic clean-libtool mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-moduleSCRIPTS + @$(NORMAL_INSTALL) + $(MAKE) $(AM_MAKEFLAGS) install-data-hook +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-moduleSCRIPTS + @$(NORMAL_INSTALL) + $(MAKE) $(AM_MAKEFLAGS) uninstall-hook +.MAKE: install-am install-data-am install-strip uninstall-am + +.PHONY: all all-am check check-am clean clean-generic clean-libtool \ + distclean distclean-generic distclean-libtool distdir dvi \ + dvi-am html html-am info info-am install install-am \ + install-data install-data-am install-data-hook install-dvi \ + install-dvi-am install-exec install-exec-am install-html \ + install-html-am install-info install-info-am install-man \ + install-moduleSCRIPTS install-pdf install-pdf-am install-ps \ + install-ps-am install-strip installcheck installcheck-am \ + installdirs maintainer-clean maintainer-clean-generic \ + mostlyclean mostlyclean-generic mostlyclean-libtool pdf pdf-am \ + ps ps-am uninstall uninstall-am uninstall-hook \ + uninstall-moduleSCRIPTS + + +install-data-hook: + for ASM in $(EXTRA_BUNDLE); do \ + $(INSTALL) -m 0755 $$ASM $(DESTDIR)$(moduledir); \ + done; + +uninstall-hook: + for ASM in $(EXTRA_BUNDLE); do \ + rm -f $(DESTDIR)$(moduledir)/`basename $$ASM`; \ + done; + +all: $(ASSEMBLY_FILE) + +run: + @pushd $(top_builddir); \ + make run; \ + popd; + +# uncommented for now. +# tests are currently excuted from Makefile in $(top_builddir) +#test: +# @pushd $(top_builddir)/tests; \ +# make $(ASSEMBLY); \ +# popd; + +build-debug: + @echo $(DEP_LINK) + +$(ASSEMBLY_FILE).mdb: $(ASSEMBLY_FILE) + +$(ASSEMBLY_FILE): $(SOURCES_BUILD) $(RESOURCES_EXPANDED) $(DEP_LINK) + @mkdir -p $(top_builddir)/bin + $(MCS) \ + $(GMCS_FLAGS) \ + $(ASSEMBLY_BUILD_FLAGS) \ + -codepage:utf8 \ + -nowarn:0278 -nowarn:0078 $$warn \ + -define:HAVE_GTK_2_10 -define:NET_2_0 \ + -debug -target:$(TARGET) -out:$@ \ + $(BUILD_DEFINES) $(ENABLE_TESTS_FLAG) $(ENABLE_ATK_FLAG) \ + $(FILTERED_LINK) $(RESOURCES_BUILD) $(SOURCES_BUILD) + @if [ -e $(srcdir)/$(notdir $@.config) ]; then \ + cp $(srcdir)/$(notdir $@.config) $(top_builddir)/bin; \ + fi; + @if [ ! -z "$(EXTRA_BUNDLE)" ]; then \ + cp $(EXTRA_BUNDLE) $(top_builddir)/bin; \ + fi; + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/SparkleLib/Scp/SparkleFetcherScp.cs b/SparkleLib/Scp/SparkleFetcherScp.cs new file mode 100644 index 0000000..d20e22a --- /dev/null +++ b/SparkleLib/Scp/SparkleFetcherScp.cs @@ -0,0 +1,139 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.IO; +using System.Diagnostics; +using System.Xml; + +namespace SparkleLib { + + // Sets up a fetcher that can get remote folders + public class SparkleFetcherScp : SparkleFetcherBase { + + public SparkleFetcherScp (string server, string remote_folder, string target_folder) : + base (server, remote_folder, target_folder) { } + + + public override bool Fetch () + { + SparkleScp scp = new SparkleScp (SparklePaths.SparkleTmpPath, + "-r \"" + base.remote_url + "\" " + "\"" + base.target_folder + "\""); + + scp.Start (); + scp.WaitForExit (); + + SparkleHelpers.DebugInfo ("Scp", "Exit code " + scp.ExitCode.ToString ()); + + if (scp.ExitCode != 0) { + return false; + } else { + InstallConfiguration (); + InstallExcludeRules (); + return true; + } + } + + + // Install the user's name and email and some config into + // the newly cloned repository + private void InstallConfiguration () + { + string log_file_path = SparkleHelpers.CombineMore (base.target_folder, ".sparkleshare", "log"); + File.Create (log_file_path); + + string config_file_path = SparkleHelpers.CombineMore (base.target_folder, ".sparkleshare", "config"); + File.Create (config_file_path); + + string config = ""; + + // Write the config to the file + TextWriter writer = new StreamWriter (config_file_path); + writer.WriteLine (config); + writer.Close (); + + SparkleHelpers.DebugInfo ("Config", "Added configuration to '" + config_file_path + "'"); + } + + + // Add a .gitignore file to the repo + private void InstallExcludeRules () + { + string exlude_rules_file_path = SparkleHelpers.CombineMore (base.target_folder, ".sparkleshare", "exclude"); + File.Create (exlude_rules_file_path); + + TextWriter writer = new StreamWriter (exlude_rules_file_path); + + // gedit and emacs + writer.WriteLine ("*~"); + + // vi(m) + writer.WriteLine (".*.sw[a-z]"); + writer.WriteLine ("*.un~"); + writer.WriteLine ("*.swp"); + writer.WriteLine ("*.swo"); + + // KDE + writer.WriteLine (".directory"); + + // Mac OSX + writer.WriteLine (".DS_Store"); + writer.WriteLine ("Icon?"); + writer.WriteLine ("._*"); + writer.WriteLine (".Spotlight-V100"); + writer.WriteLine (".Trashes"); + + // Mac OSX + writer.WriteLine ("*(Autosaved).graffle"); + + // Windows + writer.WriteLine ("Thumbs.db"); + writer.WriteLine ("Desktop.ini"); + + // CVS + writer.WriteLine ("*/CVS/*"); + writer.WriteLine (".cvsignore"); + writer.WriteLine ("*/.cvsignore"); + + // Subversion + writer.WriteLine ("/.svn/*"); + writer.WriteLine ("*/.svn/*"); + + writer.Close (); + } + } + + public class SparkleScp : Process { + + public SparkleScp (string path, string args) : base () + { + EnableRaisingEvents = true; + StartInfo.FileName = SparkleBackend.DefaultBackend.Path; + StartInfo.Arguments = args; + StartInfo.RedirectStandardOutput = true; + StartInfo.UseShellExecute = false; + StartInfo.WorkingDirectory = path; + } + + + new public void Start () + { + SparkleHelpers.DebugInfo ("Cmd", StartInfo.FileName + " " + StartInfo.Arguments); + base.Start (); + } + } +} diff --git a/SparkleLib/Scp/SparkleRepoScp.cs b/SparkleLib/Scp/SparkleRepoScp.cs new file mode 100644 index 0000000..a303055 --- /dev/null +++ b/SparkleLib/Scp/SparkleRepoScp.cs @@ -0,0 +1,115 @@ +// 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 +// 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; + +namespace SparkleLib { + + public class SparkleRepoScp : SparkleRepoBase { + + public SparkleRepoScp (string path, SparkleBackend backend) : + base (path, backend) { } + + + public override string Identifier { + get { + return "sparkles"; + } + } + + + public override string CurrentRevision { + get { + return ""; + } + } + + + public override bool CheckForRemoteChanges () + { + return true; + } + + + public override bool SyncUp () + { + return true; + } + + + public override bool SyncDown () + { + return true; + } + + + public override bool AnyDifferences { + get { + return false; + } + } + + + public override bool HasUnsyncedChanges { + get { + string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath, + ".sparkleshare", "has_unsynced_changes"); + + return File.Exists (unsynced_file_path); + } + + set { + string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath, + ".sparkleshare", "has_unsynced_changes"); + + if (value) { + if (!File.Exists (unsynced_file_path)) + File.Create (unsynced_file_path); + } else { + File.Delete (unsynced_file_path); + } + } + } + + + public override List <SparkleChangeSet> GetChangeSets (int count) + { + var l = new List<SparkleChangeSet> (); + l.Add (new SparkleChangeSet () { UserName = "test", UserEmail = "test", Revision = "test", Timestamp = DateTime.Now }); + return l; + } + + + public override void CreateInitialChangeSet () + { + base.CreateInitialChangeSet (); + } + + + public override bool UsesNotificationCenter + { + get { + string file_path = SparkleHelpers.CombineMore (LocalPath, ".sparkleshare", "disable_notification_center"); + return !File.Exists (file_path); + } + } + } +} diff --git a/SparkleLib/SparkleBackend.cs b/SparkleLib/SparkleBackend.cs new file mode 100644 index 0000000..7b5898b --- /dev/null +++ b/SparkleLib/SparkleBackend.cs @@ -0,0 +1,125 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace SparkleLib { + + public class SparkleBackend { + + public static SparkleBackend DefaultBackend = new SparkleBackendGit (); + + public string Name; + public string Path; + + + public SparkleBackend (string name, string [] paths) + { + Name = name; + + foreach (string path in paths) { + if (File.Exists (path)) { + Path = path; + break; + } + } + } + + + public bool IsPresent { + get { + return (Path != null); + } + } + + + public bool IsUsablePath (string path) + { + return (path.Length > 0); + } + + + // Strange magic needed by Platform () + [DllImport ("libc")] + static extern int uname (IntPtr buf); + + + // This fixes the PlatformID enumeration for MacOSX in Environment.OSVersion.Platform, + // which is intentionally broken in Mono for historical reasons + public static PlatformID Platform { + get { + IntPtr buf = IntPtr.Zero; + + try { + buf = Marshal.AllocHGlobal (8192); + + if (uname (buf) == 0 && Marshal.PtrToStringAnsi (buf) == "Darwin") + return PlatformID.MacOSX; + + } catch { + } finally { + if (buf != IntPtr.Zero) + Marshal.FreeHGlobal (buf); + } + + return Environment.OSVersion.Platform; + } + } + } + + + public class SparkleBackendGit : SparkleBackend { + + private static string name = "Git"; + private static string [] paths = new string [] { + "/opt/local/bin/git", + "/usr/bin/git", + "/usr/local/bin/git", + "/usr/local/git/bin/git" + }; + + public SparkleBackendGit () : base (name, paths) { } + + } + + + public class SparkleBackendHg : SparkleBackend { + + private static string name = "Hg"; + private static string [] paths = new string [] { + "/opt/local/bin/hg", + "/usr/bin/hg" + }; + + public SparkleBackendHg () : base (name, paths) { } + + } + + + public class SparkleBackendScp : SparkleBackend { + + private static string name = "Scp"; + private static string [] paths = new string [] { + "/usr/bin/scp" + }; + + public SparkleBackendScp () : base (name, paths) { } + + } +} diff --git a/SparkleLib/SparkleChangeSet.cs b/SparkleLib/SparkleChangeSet.cs new file mode 100644 index 0000000..70baabb --- /dev/null +++ b/SparkleLib/SparkleChangeSet.cs @@ -0,0 +1,37 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.Collections.Generic; + +namespace SparkleLib { + + public class SparkleChangeSet { + + public string UserName; + public string UserEmail; + public string Revision; + public DateTime Timestamp; + public bool IsMerge = false; + public List<string> Added = new List<string> (); + public List<string> Deleted = new List<string> (); + public List<string> Edited = new List<string> (); + public List<string> MovedFrom = new List<string> (); + public List<string> MovedTo = new List<string> (); + + } +} diff --git a/SparkleLib/SparkleConfig.cs b/SparkleLib/SparkleConfig.cs new file mode 100644 index 0000000..e960326 --- /dev/null +++ b/SparkleLib/SparkleConfig.cs @@ -0,0 +1,258 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.IO; +using System.Collections.Generic; +using System.Xml; + +using Mono.Unix; + +namespace SparkleLib { + + public class SparkleConfig : XmlDocument { + + public static SparkleConfig DefaultConfig = new SparkleConfig ( + SparklePaths.SparkleConfigPath, "config.xml"); + + public string Path; + + + public SparkleConfig (string config_path, string config_file_name) + { + Path = System.IO.Path.Combine (config_path, config_file_name); + + if (!Directory.Exists (config_path)) { + Directory.CreateDirectory (config_path); + SparkleHelpers.DebugInfo ("Config", "Created \"" + config_path + "\""); + } + + string icons_path = System.IO.Path.Combine (config_path, "icons"); + if (!Directory.Exists (icons_path)) { + Directory.CreateDirectory (icons_path); + SparkleHelpers.DebugInfo ("Config", "Created \"" + icons_path + "\""); + } + + if (!File.Exists (Path)) + CreateInitialConfig (); + + Load (Path); + } + + + private void CreateInitialConfig () + { + string user_name = Environment.UserName; + + if (SparkleBackend.Platform == PlatformID.Unix || + SparkleBackend.Platform == PlatformID.MacOSX) { + + user_name = new UnixUserInfo (UnixEnvironment.UserName).RealName; + user_name = user_name.TrimEnd (",".ToCharArray()); + + } + + if (string.IsNullOrEmpty (user_name)) + user_name = Environment.UserName; + + TextWriter writer = new StreamWriter (Path); + string n = Environment.NewLine; + + writer.Write ("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" + n + + "<sparkleshare>" + n + + " <user>" + n + + " <name>" + user_name + "</name>" + n + + " <email>Unknown</email>" + n + + " </user>" + n + + "</sparkleshare>"); + writer.Close (); + + SparkleHelpers.DebugInfo ("Config", "Created \"" + Path + "\""); + } + + + public string UserName { + get { + XmlNode node = SelectSingleNode ("/sparkleshare/user/name/text()"); + return node.Value; + } + + set { + XmlNode node = SelectSingleNode ("/sparkleshare/user/name/text()"); + node.InnerText = value; + + Save (); + } + } + + + public string UserEmail { + get { + XmlNode node = SelectSingleNode ("/sparkleshare/user/email/text()"); + return node.Value; + } + + set { + XmlNode node = SelectSingleNode ("/sparkleshare/user/email/text()"); + node.InnerText = value; + + Save (); + } + } + + + public List<string> Folders { + get { + List<string> folders = new List<string> (); + + foreach (XmlNode node_folder in SelectNodes ("/sparkleshare/folder")) + folders.Add (node_folder ["name"].InnerText); + + return folders; + } + } + + + public void AddFolder (string name, string url, string backend) + { + XmlNode node_name = CreateElement ("name"); + node_name.InnerText = name; + + XmlNode node_url = CreateElement ("url"); + node_url.InnerText = url; + + XmlNode node_backend = CreateElement ("backend"); + node_backend.InnerText = backend; + + XmlNode node_folder = CreateNode (XmlNodeType.Element, "folder", null); + node_folder.AppendChild (node_name); + node_folder.AppendChild (node_url); + node_folder.AppendChild (node_backend); + + XmlNode node_root = SelectSingleNode ("/sparkleshare"); + node_root.AppendChild (node_folder); + + Save (); + } + + + public void RemoveFolder (string name) + { + foreach (XmlNode node_folder in SelectNodes ("/sparkleshare/folder")) { + if (node_folder ["name"].InnerText.Equals (name)) + SelectSingleNode ("/sparkleshare").RemoveChild (node_folder); + } + + Save (); + } + + + public bool FolderExists (string name) + { + foreach (XmlNode node_folder in SelectNodes ("/sparkleshare/folder")) { + if (node_folder ["name"].InnerText.Equals (name)) + return true; + } + + return false; + } + + + public string GetBackendForFolder (string name) + { + foreach (XmlNode node_folder in SelectNodes ("/sparkleshare/folder")) { + if (node_folder ["name"].InnerText.Equals (name)) + return node_folder ["backend"].InnerText; + } + + return null; + } + + + public string GetUrlForFolder (string name) + { + foreach (XmlNode node_folder in SelectNodes ("/sparkleshare/folder")) { + if (node_folder ["name"].InnerText.Equals (name)) + return node_folder ["url"].InnerText; + } + + return null; + } + + + public string GetAnnouncementsForFolder (string name) + { + foreach (XmlNode node_folder in SelectNodes ("/sparkleshare/folder")) { + if (node_folder ["name"].InnerText.Equals (name) && + node_folder ["announcements"] != null) { + + return node_folder ["announcements"].InnerText; + } + } + + return null; + } + + + public string GetConfigOption (string name) + { + XmlNode node = SelectSingleNode ("/sparkleshare/" + name); + + if (node != null) + return node.InnerText; + else + return null; + } + + + public void SetConfigOption (string name, string content) + { + XmlNode node = SelectSingleNode ("/sparkleshare/" + name); + + if (node != null) { + node.InnerText = content; + + } else { + node = CreateElement (name); + node.InnerText = content; + + XmlNode node_root = SelectSingleNode ("/sparkleshare"); + node_root.AppendChild (node); + } + + SparkleHelpers.DebugInfo ("Config", "Updated " + name + ":" + content); + Save (); + } + + + public void Save () + { + if (!File.Exists (Path)) + throw new ConfigFileNotFoundException (Path + " does not exist"); + + Save (Path); + SparkleHelpers.DebugInfo ("Config", "Updated \"" + Path + "\""); + } + } + + + public class ConfigFileNotFoundException : Exception { + + public ConfigFileNotFoundException (string message) : + base (message) { } + } +} diff --git a/SparkleLib/SparkleFetcherBase.cs b/SparkleLib/SparkleFetcherBase.cs new file mode 100644 index 0000000..37e2984 --- /dev/null +++ b/SparkleLib/SparkleFetcherBase.cs @@ -0,0 +1,199 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.IO; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Threading; + +using Mono.Unix; + +namespace SparkleLib { + + // Sets up a fetcher that can get remote folders + public abstract class SparkleFetcherBase { + + public delegate void StartedEventHandler (); + public delegate void FinishedEventHandler (); + public delegate void FailedEventHandler (); + + public event StartedEventHandler Started; + public event FinishedEventHandler Finished; + public event FailedEventHandler Failed; + + protected string target_folder; + protected string remote_url; + private Thread thread; + + public abstract bool Fetch (); + + + public SparkleFetcherBase (string server, string remote_folder, string target_folder) + { + this.target_folder = target_folder; + this.remote_url = server + "/" + remote_folder; + } + + + // Clones the remote repository + public void Start () + { + SparkleHelpers.DebugInfo ("Fetcher", "[" + this.target_folder + "] Fetching folder: " + this.remote_url); + + if (Started != null) + Started (); + + if (Directory.Exists (this.target_folder)) + Directory.Delete (this.target_folder, true); + + string host = GetHost (this.remote_url); + + if (String.IsNullOrEmpty (host)) { + if (Failed != null) + Failed (); + + return; + } + + DisableHostKeyCheckingForHost (host); + + this.thread = new Thread (new ThreadStart (delegate { + if (Fetch ()) { + SparkleHelpers.DebugInfo ("Fetcher", "Finished"); + + EnableHostKeyCheckingForHost (host); + + if (Finished != null) + Finished (); + + } else { + SparkleHelpers.DebugInfo ("Fetcher", "Failed"); + + EnableHostKeyCheckingForHost (host); + + if (Failed != null) + Failed (); + } + })); + + this.thread.Start (); + } + + + public string RemoteUrl { + get { + return this.remote_url; + } + } + + + public void Dispose () + { + if (this.thread != null) { + this.thread.Abort (); + this.thread.Join (); + } + } + + + private void DisableHostKeyCheckingForHost (string host) + { + string path = SparklePaths.HomePath; + + if (!(SparkleBackend.Platform == PlatformID.Unix || + SparkleBackend.Platform == PlatformID.MacOSX)) { + + path = Environment.ExpandEnvironmentVariables ("%HOMEDRIVE%%HOMEPATH%"); + } + + string ssh_config_file_path = SparkleHelpers.CombineMore (path, ".ssh", "config"); + string ssh_config = Environment.NewLine + "Host " + host + + Environment.NewLine + "\tStrictHostKeyChecking no"; + + if (File.Exists (ssh_config_file_path)) { + TextWriter writer = File.AppendText (ssh_config_file_path); + writer.WriteLine (ssh_config); + writer.Close (); + + } else { + TextWriter writer = new StreamWriter (ssh_config_file_path); + writer.WriteLine (ssh_config); + writer.Close (); + } + + UnixFileSystemInfo file_info = new UnixFileInfo (ssh_config_file_path); + file_info.FileAccessPermissions = (FileAccessPermissions.UserRead | + FileAccessPermissions.UserWrite); + + SparkleHelpers.DebugInfo ("Fetcher", "Disabled host key checking"); + } + + + private void EnableHostKeyCheckingForHost (string host) + { + string path = SparklePaths.HomePath; + + if (!(SparkleBackend.Platform == PlatformID.Unix || + SparkleBackend.Platform == PlatformID.MacOSX)) { + + path = Environment.ExpandEnvironmentVariables ("%HOMEDRIVE%%HOMEPATH%"); + } + + string ssh_config_file_path = SparkleHelpers.CombineMore (path, ".ssh", "config"); + string ssh_config = Environment.NewLine + "Host " + host + + Environment.NewLine + "\tStrictHostKeyChecking no"; + + if (File.Exists (ssh_config_file_path)) { + StreamReader reader = new StreamReader (ssh_config_file_path); + string current_ssh_config = reader.ReadToEnd (); + reader.Close (); + + current_ssh_config = current_ssh_config.Remove ( + current_ssh_config.IndexOf (ssh_config), ssh_config.Length); + + bool has_some_ssh_config = new Regex (@"[a-z]").IsMatch (current_ssh_config); + if (!has_some_ssh_config) { + File.Delete (ssh_config_file_path); + + } else { + TextWriter writer = new StreamWriter (ssh_config_file_path); + writer.WriteLine (current_ssh_config); + writer.Close (); + + UnixFileSystemInfo file_info = new UnixFileInfo (ssh_config_file_path); + file_info.FileAccessPermissions = (FileAccessPermissions.UserRead | + FileAccessPermissions.UserWrite); + } + } + + SparkleHelpers.DebugInfo ("Fetcher", "Enabled host key checking"); + } + + + private string GetHost (string url) + { + Regex regex = new Regex (@"(@|://)([a-z0-9\.-]+)(/|:)"); + Match match = regex.Match (url); + + if (match.Success) + return match.Groups [2].Value; + else + return null; + } + } +} diff --git a/SparkleLib/SparkleHelpers.cs b/SparkleLib/SparkleHelpers.cs new file mode 100644 index 0000000..741648d --- /dev/null +++ b/SparkleLib/SparkleHelpers.cs @@ -0,0 +1,80 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.IO; + +namespace SparkleLib { + + public static class SparkleHelpers { + + public static bool ShowDebugInfo = true; + + + // Show debug info if needed + public static void DebugInfo (string type, string message) + { + if (ShowDebugInfo) { + string timestamp = DateTime.Now.ToString ("HH:mm:ss"); + + if (!message.StartsWith ("[")) + message = " " + message; + + // TODO: Write to a log + Console.WriteLine (timestamp + " " + "[" + type + "]" + message); + } + } + + + // Makes it possible to combine more than + // two paths at once + public static string CombineMore (params string [] parts) + { + string new_path = ""; + + foreach (string part in parts) + new_path = Path.Combine (new_path, part); + + return new_path; + } + + + // Recursively sets access rights of a folder to 'Normal' + public static void ClearAttributes (string path) + { + if (Directory.Exists (path)) { + string [] folders = Directory .GetDirectories (path); + + foreach (string folder in folders) + ClearAttributes (folder); + + string [] files = Directory .GetFiles(path); + + foreach (string file in files) + File.SetAttributes (file, FileAttributes.Normal); + } + } + + + // Converts a UNIX timestamp to a more usable time object + public static DateTime UnixTimestampToDateTime (int timestamp) + { + DateTime unix_epoch = new DateTime (1970, 1, 1, 0, 0, 0, 0); + return unix_epoch.AddSeconds (timestamp); + } + } +} diff --git a/SparkleLib/SparkleListenerBase.cs b/SparkleLib/SparkleListenerBase.cs new file mode 100644 index 0000000..bcf12c9 --- /dev/null +++ b/SparkleLib/SparkleListenerBase.cs @@ -0,0 +1,201 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.Collections.Generic; +using System.Timers; + +namespace SparkleLib { + + public class SparkleAnnouncement { + + public readonly string FolderIdentifier; + public readonly string Message; + + + public SparkleAnnouncement (string folder_identifier, string message) + { + FolderIdentifier = folder_identifier; + Message = message; + } + } + + + public static class SparkleListenerFactory { + + private static List<SparkleListenerBase> listeners; + + public static SparkleListenerIrc CreateIrcListener (string server, string folder_identifier, + string announcements) + { + if (listeners == null) + listeners = new List<SparkleListenerBase> (); + + // This is SparkleShare's centralized notification service. + // Don't worry, we only use this server as a backup if you + // don't have your own. All data needed to connect is hashed and + // we don't store any personal information ever + if (announcements == null) + server = "204.62.14.135"; + else + server = announcements; + + foreach (SparkleListenerBase listener in listeners) { + if (listener.Server.Equals (server)) { + SparkleHelpers.DebugInfo ("ListenerFactory", "Refered to existing listener for " + server); + listener.AlsoListenTo (folder_identifier); + return (SparkleListenerIrc) listener; + } + } + + SparkleHelpers.DebugInfo ("ListenerFactory", "Issued new listener for " + server); + listeners.Add (new SparkleListenerIrc (server, folder_identifier, announcements)); + return (SparkleListenerIrc) listeners [listeners.Count - 1]; + } + } + + + // A persistent connection to the server that + // listens for change notifications + public abstract class SparkleListenerBase { + + // We've connected to the server + public event ConnectedEventHandler Connected; + public delegate void ConnectedEventHandler (); + + // We've disconnected from the server + public event DisconnectedEventHandler Disconnected; + public delegate void DisconnectedEventHandler (); + + // We've been notified about a remote + // change by the channel + public event AnnouncementEventHandler Announcement; + public delegate void AnnouncementEventHandler (SparkleAnnouncement announcement); + + + public abstract void Connect (); + public abstract void Announce (SparkleAnnouncement announcent); + public abstract void AlsoListenTo (string folder_identifier); + public abstract bool IsConnected { get; } + + + protected List<string> channels = new List<string> (); + protected List<SparkleAnnouncement> queue_up = new List<SparkleAnnouncement> (); + protected List<SparkleAnnouncement> queue_down = new List<SparkleAnnouncement> (); + protected bool is_connecting; + protected string server; + protected Timer reconnect_timer = new Timer { Interval = 60 * 1000, Enabled = true }; + + public SparkleListenerBase (string server, string folder_identifier, string announcements) { + this.reconnect_timer.Elapsed += delegate { + if (!IsConnected && !this.is_connecting) + Reconnect (); + }; + + this.reconnect_timer.Start (); + } + + + public void AnnounceBase (SparkleAnnouncement announcement) { + if (IsConnected) { + SparkleHelpers.DebugInfo ("Listener", "Announcing to " + announcement.FolderIdentifier + " on " + this.server); + Announce (announcement); + + } else { + SparkleHelpers.DebugInfo ("Listener", "Not connected to " + this.server + ". Queuing message"); + this.queue_up.Add (announcement); + } + } + + + public bool HasQueueDownAnnouncement (string folder_identifier) + { + foreach (SparkleAnnouncement announcement in this.queue_down) { + if (announcement.FolderIdentifier.Equals (folder_identifier)) { + this.queue_down.Remove (announcement); + return true; + } + } + + return false; + } + + + public void Reconnect () + { + SparkleHelpers.DebugInfo ("Listener", "Trying to reconnect to " + this.server); + Connect (); + } + + + public void OnConnected () + { + SparkleHelpers.DebugInfo ("Listener", "Connected to " + Server); + + if (Connected != null) + Connected (); + + if (this.queue_up.Count > 0) { + SparkleHelpers.DebugInfo ("Listener", "Delivering queued messages..."); + foreach (SparkleAnnouncement announcement in this.queue_up) { + AnnounceBase (announcement); + this.queue_up.Remove (announcement); + } + } + } + + + public void OnDisconnected () + { + SparkleHelpers.DebugInfo ("Listener", "Disonnected"); + + if (Disconnected != null) + Disconnected (); + } + + + public void OnAnnouncement (SparkleAnnouncement announcement) + { + SparkleHelpers.DebugInfo ("Listener", "Got message from " + announcement.FolderIdentifier + " on " + this.server); + + this.queue_down.Add (announcement); + + if (Announcement != null) + Announcement (announcement); + } + + + public virtual void Dispose () + { + this.reconnect_timer.Dispose (); + } + + + public string Server { + get { + return this.server; + } + } + + + public bool IsConnecting { + get { + return this.is_connecting; + } + } + } +} diff --git a/SparkleLib/SparkleListenerIrc.cs b/SparkleLib/SparkleListenerIrc.cs new file mode 100644 index 0000000..8ab5f5b --- /dev/null +++ b/SparkleLib/SparkleListenerIrc.cs @@ -0,0 +1,160 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.Text; +using System.Threading; +using System.Security.Cryptography; + +using Meebey.SmartIrc4net; + +namespace SparkleLib { + + public class SparkleListenerIrc : SparkleListenerBase { + + private Thread thread; + private IrcClient client; + private string nick; + + + public SparkleListenerIrc (string server, string folder_identifier, string announcements) : + base (server, folder_identifier, announcements) + { + base.server = server; + + // Try to get a uniqueish nickname + this.nick = SHA1 (DateTime.Now.ToString ("ffffff") + "sparkles"); + + // Most irc servers don't allow nicknames starting + // with a number, so prefix an alphabetic character + this.nick = "s" + this.nick.Substring (0, 7); + + base.channels.Add ("#" + folder_identifier); + + this.client = new IrcClient () { + PingTimeout = 180, + PingInterval = 60 + }; + + this.client.OnConnected += delegate { + base.is_connecting = false; + OnConnected (); + }; + + this.client.OnDisconnected += delegate { + base.is_connecting = false; + OnDisconnected (); + }; + + this.client.OnError += delegate { + base.is_connecting = false; + OnDisconnected (); + }; + + this.client.OnChannelMessage += delegate (object o, IrcEventArgs args) { + string message = args.Data.Message.Trim (); + string folder_id = args.Data.Channel.Substring (1); // remove the starting hash + OnAnnouncement (new SparkleAnnouncement (folder_id, message)); + }; + } + + + public override bool IsConnected { + get { + return this.client.IsConnected; + } + } + + + // Starts a new thread and listens to the channel + public override void Connect () + { + SparkleHelpers.DebugInfo ("ListenerIrc", "Connecting to " + Server); + + base.is_connecting = true; + + this.thread = new Thread ( + new ThreadStart (delegate { + try { + + // Connect, login, and join the channel + this.client.Connect (new string [] {base.server}, 6667); + this.client.Login (this.nick, this.nick); + + foreach (string channel in base.channels) { + SparkleHelpers.DebugInfo ("ListenerIrc", "Joining channel " + channel); + this.client.RfcJoin (channel); + } + + // List to the channel, this blocks the thread + this.client.Listen (); + + // Disconnect when we time out + this.client.Disconnect (); + + } catch (ConnectionException e) { + SparkleHelpers.DebugInfo ("ListenerIrc", "Could not connect to " + Server + ": " + e.Message); + } + }) + ); + + this.thread.Start (); + } + + + public override void AlsoListenTo (string folder_identifier) + { + string channel = "#" + folder_identifier; + if (!base.channels.Contains (channel)) { + base.channels.Add (channel); + + if (IsConnected) { + SparkleHelpers.DebugInfo ("ListenerIrc", "Joining channel " + channel); + this.client.RfcJoin (channel); + } + } + } + + + public override void Announce (SparkleAnnouncement announcement) + { + string channel = "#" + announcement.FolderIdentifier; + this.client.SendMessage (SendType.Message, channel, announcement.Message); + + // Also announce to ourselves for debugging purposes + // base.OnAnnouncement (announcement); + } + + + public override void Dispose () + { + this.thread.Abort (); + this.thread.Join (); + base.Dispose (); + } + + + // Creates a SHA-1 hash of input + private string SHA1 (string s) + { + SHA1 sha1 = new SHA1CryptoServiceProvider (); + Byte[] bytes = ASCIIEncoding.Default.GetBytes (s); + Byte[] encoded_bytes = sha1.ComputeHash (bytes); + return BitConverter.ToString (encoded_bytes).ToLower ().Replace ("-", ""); + } + } +} diff --git a/SparkleLib/SparkleOptions.cs b/SparkleLib/SparkleOptions.cs new file mode 100644 index 0000000..b071c42 --- /dev/null +++ b/SparkleLib/SparkleOptions.cs @@ -0,0 +1,1101 @@ +// +// Options.cs +// +// Authors: +// Jonathan Pryor <jpryor@novell.com> +// +// Copyright (C) 2008 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +// Compile With: +// gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll +// gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll +// +// The LINQ version just changes the implementation of +// OptionSet.Parse(IEnumerable<string>), and confers no semantic changes. + +// +// A Getopt::Long-inspired option parsing library for C#. +// +// NDesk.Options.OptionSet is built upon a key/value table, where the +// key is a option format string and the value is a delegate that is +// invoked when the format string is matched. +// +// Option format strings: +// Regex-like BNF Grammar: +// name: .+ +// type: [=:] +// sep: ( [^{}]+ | '{' .+ '}' )? +// aliases: ( name type sep ) ( '|' name type sep )* +// +// Each '|'-delimited name is an alias for the associated action. If the +// format string ends in a '=', it has a required value. If the format +// string ends in a ':', it has an optional value. If neither '=' or ':' +// is present, no value is supported. `=' or `:' need only be defined on one +// alias, but if they are provided on more than one they must be consistent. +// +// Each alias portion may also end with a "key/value separator", which is used +// to split option values if the option accepts > 1 value. If not specified, +// it defaults to '=' and ':'. If specified, it can be any character except +// '{' and '}' OR the *string* between '{' and '}'. If no separator should be +// used (i.e. the separate values should be distinct arguments), then "{}" +// should be used as the separator. +// +// Options are extracted either from the current option by looking for +// the option name followed by an '=' or ':', or is taken from the +// following option IFF: +// - The current option does not contain a '=' or a ':' +// - The current option requires a value (i.e. not a Option type of ':') +// +// The `name' used in the option format string does NOT include any leading +// option indicator, such as '-', '--', or '/'. All three of these are +// permitted/required on any named option. +// +// Option bundling is permitted so long as: +// - '-' is used to start the option group +// - all of the bundled options are a single character +// - at most one of the bundled options accepts a value, and the value +// provided starts from the next character to the end of the string. +// +// This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value' +// as '-Dname=value'. +// +// Option processing is disabled by specifying "--". All options after "--" +// are returned by OptionSet.Parse() unchanged and unprocessed. +// +// Unprocessed options are returned from OptionSet.Parse(). +// +// Examples: +// int verbose = 0; +// OptionSet p = new OptionSet () +// .Add ("v", v => ++verbose) +// .Add ("name=|value=", v => Console.WriteLine (v)); +// p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"}); +// +// The above would parse the argument string array, and would invoke the +// lambda expression three times, setting `verbose' to 3 when complete. +// It would also print out "A" and "B" to standard output. +// The returned array would contain the string "extra". +// +// C# 3.0 collection initializers are supported and encouraged: +// var p = new OptionSet () { +// { "h|?|help", v => ShowHelp () }, +// }; +// +// System.ComponentModel.TypeConverter is also supported, allowing the use of +// custom data types in the callback type; TypeConverter.ConvertFromString() +// is used to convert the value option to an instance of the specified +// type: +// +// var p = new OptionSet () { +// { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) }, +// }; +// +// Random other tidbits: +// - Boolean options (those w/o '=' or ':' in the option format string) +// are explicitly enabled if they are followed with '+', and explicitly +// disabled if they are followed with '-': +// string a = null; +// var p = new OptionSet () { +// { "a", s => a = s }, +// }; +// p.Parse (new string[]{"-a"}); // sets v != null +// p.Parse (new string[]{"-a+"}); // sets v != null +// p.Parse (new string[]{"-a-"}); // sets v == null +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Security.Permissions; +using System.Text; +using System.Text.RegularExpressions; + +#if LINQ +using System.Linq; +#endif + +#if TEST +using NDesk.Options; +#endif + +#if NDESK_OPTIONS +namespace NDesk.Options +#else +namespace SparkleLib.Options +#endif +{ + public class OptionValueCollection : IList, IList<string> { + + List<string> values = new List<string> (); + OptionContext c; + + internal OptionValueCollection (OptionContext c) + { + this.c = c; + } + + #region ICollection + void ICollection.CopyTo (Array array, int index) {(values as ICollection).CopyTo (array, index);} + bool ICollection.IsSynchronized {get {return (values as ICollection).IsSynchronized;}} + object ICollection.SyncRoot {get {return (values as ICollection).SyncRoot;}} + #endregion + + #region ICollection<T> + public void Add (string item) {values.Add (item);} + public void Clear () {values.Clear ();} + public bool Contains (string item) {return values.Contains (item);} + public void CopyTo (string[] array, int arrayIndex) {values.CopyTo (array, arrayIndex);} + public bool Remove (string item) {return values.Remove (item);} + public int Count {get {return values.Count;}} + public bool IsReadOnly {get {return false;}} + #endregion + + #region IEnumerable + IEnumerator IEnumerable.GetEnumerator () {return values.GetEnumerator ();} + #endregion + + #region IEnumerable<T> + public IEnumerator<string> GetEnumerator () {return values.GetEnumerator ();} + #endregion + + #region IList + int IList.Add (object value) {return (values as IList).Add (value);} + bool IList.Contains (object value) {return (values as IList).Contains (value);} + int IList.IndexOf (object value) {return (values as IList).IndexOf (value);} + void IList.Insert (int index, object value) {(values as IList).Insert (index, value);} + void IList.Remove (object value) {(values as IList).Remove (value);} + void IList.RemoveAt (int index) {(values as IList).RemoveAt (index);} + bool IList.IsFixedSize {get {return false;}} + object IList.this [int index] {get {return this [index];} set {(values as IList)[index] = value;}} + #endregion + + #region IList<T> + public int IndexOf (string item) {return values.IndexOf (item);} + public void Insert (int index, string item) {values.Insert (index, item);} + public void RemoveAt (int index) {values.RemoveAt (index);} + + private void AssertValid (int index) + { + if (c.Option == null) + throw new InvalidOperationException ("OptionContext.Option is null."); + if (index >= c.Option.MaxValueCount) + throw new ArgumentOutOfRangeException ("index"); + if (c.Option.OptionValueType == OptionValueType.Required && + index >= values.Count) + throw new OptionException (string.Format ( + c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName), + c.OptionName); + } + + public string this [int index] { + get { + AssertValid (index); + return index >= values.Count ? null : values [index]; + } + set { + values [index] = value; + } + } + #endregion + + public List<string> ToList () + { + return new List<string> (values); + } + + public string[] ToArray () + { + return values.ToArray (); + } + + public override string ToString () + { + return string.Join (", ", values.ToArray ()); + } + } + + public class OptionContext { + private Option option; + private string name; + private int index; + private OptionSet set; + private OptionValueCollection c; + + public OptionContext (OptionSet set) + { + this.set = set; + this.c = new OptionValueCollection (this); + } + + public Option Option { + get {return option;} + set {option = value;} + } + + public string OptionName { + get {return name;} + set {name = value;} + } + + public int OptionIndex { + get {return index;} + set {index = value;} + } + + public OptionSet OptionSet { + get {return set;} + } + + public OptionValueCollection OptionValues { + get {return c;} + } + } + + public enum OptionValueType { + None, + Optional, + Required, + } + + public abstract class Option { + string prototype, description; + string[] names; + OptionValueType type; + int count; + string[] separators; + + protected Option (string prototype, string description) + : this (prototype, description, 1) + { + } + + protected Option (string prototype, string description, int maxValueCount) + { + if (prototype == null) + throw new ArgumentNullException ("prototype"); + if (prototype.Length == 0) + throw new ArgumentException ("Cannot be the empty string.", "prototype"); + if (maxValueCount < 0) + throw new ArgumentOutOfRangeException ("maxValueCount"); + + this.prototype = prototype; + this.names = prototype.Split ('|'); + this.description = description; + this.count = maxValueCount; + this.type = ParsePrototype (); + + if (this.count == 0 && type != OptionValueType.None) + throw new ArgumentException ( + "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + + "OptionValueType.Optional.", + "maxValueCount"); + if (this.type == OptionValueType.None && maxValueCount > 1) + throw new ArgumentException ( + string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), + "maxValueCount"); + if (Array.IndexOf (names, "<>") >= 0 && + ((names.Length == 1 && this.type != OptionValueType.None) || + (names.Length > 1 && this.MaxValueCount > 1))) + throw new ArgumentException ( + "The default option handler '<>' cannot require values.", + "prototype"); + } + + public string Prototype {get {return prototype;}} + public string Description {get {return description;}} + public OptionValueType OptionValueType {get {return type;}} + public int MaxValueCount {get {return count;}} + + public string[] GetNames () + { + return (string[]) names.Clone (); + } + + public string[] GetValueSeparators () + { + if (separators == null) + return new string [0]; + return (string[]) separators.Clone (); + } + + protected static T Parse<T> (string value, OptionContext c) + { + Type tt = typeof (T); + bool nullable = tt.IsValueType && tt.IsGenericType && + !tt.IsGenericTypeDefinition && + tt.GetGenericTypeDefinition () == typeof (Nullable<>); + Type targetType = nullable ? tt.GetGenericArguments () [0] : typeof (T); + TypeConverter conv = TypeDescriptor.GetConverter (targetType); + T t = default (T); + try { + if (value != null) + t = (T) conv.ConvertFromString (value); + } + catch (Exception e) { + throw new OptionException ( + string.Format ( + c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."), + value, targetType.Name, c.OptionName), + c.OptionName, e); + } + return t; + } + + internal string[] Names {get {return names;}} + internal string[] ValueSeparators {get {return separators;}} + + static readonly char[] NameTerminator = new char[]{'=', ':'}; + + private OptionValueType ParsePrototype () + { + char type = '\0'; + List<string> seps = new List<string> (); + for (int i = 0; i < names.Length; ++i) { + string name = names [i]; + if (name.Length == 0) + throw new ArgumentException ("Empty option names are not supported.", "prototype"); + + int end = name.IndexOfAny (NameTerminator); + if (end == -1) + continue; + names [i] = name.Substring (0, end); + if (type == '\0' || type == name [end]) + type = name [end]; + else + throw new ArgumentException ( + string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]), + "prototype"); + AddSeparators (name, end, seps); + } + + if (type == '\0') + return OptionValueType.None; + + if (count <= 1 && seps.Count != 0) + throw new ArgumentException ( + string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count), + "prototype"); + if (count > 1) { + if (seps.Count == 0) + this.separators = new string[]{":", "="}; + else if (seps.Count == 1 && seps [0].Length == 0) + this.separators = null; + else + this.separators = seps.ToArray (); + } + + return type == '=' ? OptionValueType.Required : OptionValueType.Optional; + } + + private static void AddSeparators (string name, int end, ICollection<string> seps) + { + int start = -1; + for (int i = end+1; i < name.Length; ++i) { + switch (name [i]) { + case '{': + if (start != -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + start = i+1; + break; + case '}': + if (start == -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + seps.Add (name.Substring (start, i-start)); + start = -1; + break; + default: + if (start == -1) + seps.Add (name [i].ToString ()); + break; + } + } + if (start != -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + } + + public void Invoke (OptionContext c) + { + OnParseComplete (c); + c.OptionName = null; + c.Option = null; + c.OptionValues.Clear (); + } + + protected abstract void OnParseComplete (OptionContext c); + + public override string ToString () + { + return Prototype; + } + } + + [Serializable] + public class OptionException : Exception { + private string option; + + public OptionException () + { + } + + public OptionException (string message, string optionName) + : base (message) + { + this.option = optionName; + } + + public OptionException (string message, string optionName, Exception innerException) + : base (message, innerException) + { + this.option = optionName; + } + + protected OptionException (SerializationInfo info, StreamingContext context) + : base (info, context) + { + this.option = info.GetString ("OptionName"); + } + + public string OptionName { + get {return this.option;} + } + + [SecurityPermission (SecurityAction.LinkDemand, SerializationFormatter = true)] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + info.AddValue ("OptionName", option); + } + } + + public delegate void OptionAction<TKey, TValue> (TKey key, TValue value); + + public class OptionSet : KeyedCollection<string, Option> + { + public OptionSet () + : this (delegate (string f) {return f;}) + { + } + + public OptionSet (Converter<string, string> localizer) + { + this.localizer = localizer; + } + + Converter<string, string> localizer; + + public Converter<string, string> MessageLocalizer { + get {return localizer;} + } + + protected override string GetKeyForItem (Option item) + { + if (item == null) + throw new ArgumentNullException ("option"); + if (item.Names != null && item.Names.Length > 0) + return item.Names [0]; + // This should never happen, as it's invalid for Option to be + // constructed w/o any names. + throw new InvalidOperationException ("Option has no names!"); + } + + [Obsolete ("Use KeyedCollection.this[string]")] + protected Option GetOptionForName (string option) + { + if (option == null) + throw new ArgumentNullException ("option"); + try { + return base [option]; + } + catch (KeyNotFoundException) { + return null; + } + } + + protected override void InsertItem (int index, Option item) + { + base.InsertItem (index, item); + AddImpl (item); + } + + protected override void RemoveItem (int index) + { + base.RemoveItem (index); + Option p = Items [index]; + // KeyedCollection.RemoveItem() handles the 0th item + for (int i = 1; i < p.Names.Length; ++i) { + Dictionary.Remove (p.Names [i]); + } + } + + protected override void SetItem (int index, Option item) + { + base.SetItem (index, item); + RemoveItem (index); + AddImpl (item); + } + + private void AddImpl (Option option) + { + if (option == null) + throw new ArgumentNullException ("option"); + List<string> added = new List<string> (option.Names.Length); + try { + // KeyedCollection.InsertItem/SetItem handle the 0th name. + for (int i = 1; i < option.Names.Length; ++i) { + Dictionary.Add (option.Names [i], option); + added.Add (option.Names [i]); + } + } + catch (Exception) { + foreach (string name in added) + Dictionary.Remove (name); + throw; + } + } + + public new OptionSet Add (Option option) + { + base.Add (option); + return this; + } + + sealed class ActionOption : Option { + Action<OptionValueCollection> action; + + public ActionOption (string prototype, string description, int count, Action<OptionValueCollection> action) + : base (prototype, description, count) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action (c.OptionValues); + } + } + + public OptionSet Add (string prototype, Action<string> action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, Action<string> action) + { + if (action == null) + throw new ArgumentNullException ("action"); + Option p = new ActionOption (prototype, description, 1, + delegate (OptionValueCollection v) { action (v [0]); }); + base.Add (p); + return this; + } + + public OptionSet Add (string prototype, OptionAction<string, string> action) + { + return Add (prototype, null, action); + } + + public OptionSet Add (string prototype, string description, OptionAction<string, string> action) + { + if (action == null) + throw new ArgumentNullException ("action"); + Option p = new ActionOption (prototype, description, 2, + delegate (OptionValueCollection v) {action (v [0], v [1]);}); + base.Add (p); + return this; + } + + sealed class ActionOption<T> : Option { + Action<T> action; + + public ActionOption (string prototype, string description, Action<T> action) + : base (prototype, description, 1) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action (Parse<T> (c.OptionValues [0], c)); + } + } + + sealed class ActionOption<TKey, TValue> : Option { + OptionAction<TKey, TValue> action; + + public ActionOption (string prototype, string description, OptionAction<TKey, TValue> action) + : base (prototype, description, 2) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action ( + Parse<TKey> (c.OptionValues [0], c), + Parse<TValue> (c.OptionValues [1], c)); + } + } + + public OptionSet Add<T> (string prototype, Action<T> action) + { + return Add (prototype, null, action); + } + + public OptionSet Add<T> (string prototype, string description, Action<T> action) + { + return Add (new ActionOption<T> (prototype, description, action)); + } + + public OptionSet Add<TKey, TValue> (string prototype, OptionAction<TKey, TValue> action) + { + return Add (prototype, null, action); + } + + public OptionSet Add<TKey, TValue> (string prototype, string description, OptionAction<TKey, TValue> action) + { + return Add (new ActionOption<TKey, TValue> (prototype, description, action)); + } + + protected virtual OptionContext CreateOptionContext () + { + return new OptionContext (this); + } + +#if LINQ + public List<string> Parse (IEnumerable<string> arguments) + { + bool process = true; + OptionContext c = CreateOptionContext (); + c.OptionIndex = -1; + var def = GetOptionForName ("<>"); + var unprocessed = + from argument in arguments + where ++c.OptionIndex >= 0 && (process || def != null) + ? process + ? argument == "--" + ? (process = false) + : !Parse (argument, c) + ? def != null + ? Unprocessed (null, def, c, argument) + : true + : false + : def != null + ? Unprocessed (null, def, c, argument) + : true + : true + select argument; + List<string> r = unprocessed.ToList (); + if (c.Option != null) + c.Option.Invoke (c); + return r; + } +#else + public List<string> Parse (IEnumerable<string> arguments) + { + OptionContext c = CreateOptionContext (); + c.OptionIndex = -1; + bool process = true; + List<string> unprocessed = new List<string> (); + Option def = Contains ("<>") ? this ["<>"] : null; + foreach (string argument in arguments) { + ++c.OptionIndex; + if (argument == "--") { + process = false; + continue; + } + if (!process) { + Unprocessed (unprocessed, def, c, argument); + continue; + } + if (!Parse (argument, c)) + Unprocessed (unprocessed, def, c, argument); + } + if (c.Option != null) + c.Option.Invoke (c); + return unprocessed; + } +#endif + + private static bool Unprocessed (ICollection<string> extra, Option def, OptionContext c, string argument) + { + if (def == null) { + extra.Add (argument); + return false; + } + c.OptionValues.Add (argument); + c.Option = def; + c.Option.Invoke (c); + return false; + } + + private readonly Regex ValueOption = new Regex ( + @"^(?<flag>--|-|/)(?<name>[^:=]+)((?<sep>[:=])(?<value>.*))?$"); + + protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value) + { + if (argument == null) + throw new ArgumentNullException ("argument"); + + flag = name = sep = value = null; + Match m = ValueOption.Match (argument); + if (!m.Success) { + return false; + } + flag = m.Groups ["flag"].Value; + name = m.Groups ["name"].Value; + if (m.Groups ["sep"].Success && m.Groups ["value"].Success) { + sep = m.Groups ["sep"].Value; + value = m.Groups ["value"].Value; + } + return true; + } + + protected virtual bool Parse (string argument, OptionContext c) + { + if (c.Option != null) { + ParseValue (argument, c); + return true; + } + + string f, n, s, v; + if (!GetOptionParts (argument, out f, out n, out s, out v)) + return false; + + Option p; + if (Contains (n)) { + p = this [n]; + c.OptionName = f + n; + c.Option = p; + switch (p.OptionValueType) { + case OptionValueType.None: + c.OptionValues.Add (n); + c.Option.Invoke (c); + break; + case OptionValueType.Optional: + case OptionValueType.Required: + ParseValue (v, c); + break; + } + return true; + } + // no match; is it a bool option? + if (ParseBool (argument, n, c)) + return true; + // is it a bundled option? + if (ParseBundledValue (f, string.Concat (n + s + v), c)) + return true; + + return false; + } + + private void ParseValue (string option, OptionContext c) + { + if (option != null) + foreach (string o in c.Option.ValueSeparators != null + ? option.Split (c.Option.ValueSeparators, StringSplitOptions.None) + : new string[]{option}) { + c.OptionValues.Add (o); + } + if (c.OptionValues.Count == c.Option.MaxValueCount || + c.Option.OptionValueType == OptionValueType.Optional) + c.Option.Invoke (c); + else if (c.OptionValues.Count > c.Option.MaxValueCount) { + throw new OptionException (localizer (string.Format ( + "Error: Found {0} option values when expecting {1}.", + c.OptionValues.Count, c.Option.MaxValueCount)), + c.OptionName); + } + } + + private bool ParseBool (string option, string n, OptionContext c) + { + Option p; + string rn; + if (n.Length >= 1 && (n [n.Length-1] == '+' || n [n.Length-1] == '-') && + Contains ((rn = n.Substring (0, n.Length-1)))) { + p = this [rn]; + string v = n [n.Length-1] == '+' ? option : null; + c.OptionName = option; + c.Option = p; + c.OptionValues.Add (v); + p.Invoke (c); + return true; + } + return false; + } + + private bool ParseBundledValue (string f, string n, OptionContext c) + { + if (f != "-") + return false; + for (int i = 0; i < n.Length; ++i) { + Option p; + string opt = f + n [i].ToString (); + string rn = n [i].ToString (); + if (!Contains (rn)) { + if (i == 0) + return false; + throw new OptionException (string.Format (localizer ( + "Cannot bundle unregistered option '{0}'."), opt), opt); + } + p = this [rn]; + switch (p.OptionValueType) { + case OptionValueType.None: + Invoke (c, opt, n, p); + break; + case OptionValueType.Optional: + case OptionValueType.Required: { + string v = n.Substring (i+1); + c.Option = p; + c.OptionName = opt; + ParseValue (v.Length != 0 ? v : null, c); + return true; + } + default: + throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType); + } + } + return true; + } + + private static void Invoke (OptionContext c, string name, string value, Option option) + { + c.OptionName = name; + c.Option = option; + c.OptionValues.Add (value); + option.Invoke (c); + } + + private const int OptionWidth = 29; + + public void WriteOptionDescriptions (TextWriter o) + { + foreach (Option p in this) { + int written = 0; + if (!WriteOptionPrototype (o, p, ref written)) + continue; + + if (written < OptionWidth) + o.Write (new string (' ', OptionWidth - written)); + else { + o.WriteLine (); + o.Write (new string (' ', OptionWidth)); + } + + bool indent = false; + string prefix = new string (' ', OptionWidth+2); + foreach (string line in GetLines (localizer (GetDescription (p.Description)))) { + if (indent) + o.Write (prefix); + o.WriteLine (line); + indent = true; + } + } + } + + bool WriteOptionPrototype (TextWriter o, Option p, ref int written) + { + string[] names = p.Names; + + int i = GetNextOptionIndex (names, 0); + if (i == names.Length) + return false; + + if (names [i].Length == 1) { + Write (o, ref written, " -"); + Write (o, ref written, names [0]); + } + else { + Write (o, ref written, " --"); + Write (o, ref written, names [0]); + } + + for ( i = GetNextOptionIndex (names, i+1); + i < names.Length; i = GetNextOptionIndex (names, i+1)) { + Write (o, ref written, ", "); + Write (o, ref written, names [i].Length == 1 ? "-" : "--"); + Write (o, ref written, names [i]); + } + + if (p.OptionValueType == OptionValueType.Optional || + p.OptionValueType == OptionValueType.Required) { + if (p.OptionValueType == OptionValueType.Optional) { + Write (o, ref written, localizer ("[")); + } + Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description))); + string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 + ? p.ValueSeparators [0] + : " "; + for (int c = 1; c < p.MaxValueCount; ++c) { + Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description))); + } + if (p.OptionValueType == OptionValueType.Optional) { + Write (o, ref written, localizer ("]")); + } + } + return true; + } + + static int GetNextOptionIndex (string[] names, int i) + { + while (i < names.Length && names [i] == "<>") { + ++i; + } + return i; + } + + static void Write (TextWriter o, ref int n, string s) + { + n += s.Length; + o.Write (s); + } + + private static string GetArgumentName (int index, int maxIndex, string description) + { + if (description == null) + return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); + string[] nameStart; + if (maxIndex == 1) + nameStart = new string[]{"{0:", "{"}; + else + nameStart = new string[]{"{" + index + ":"}; + for (int i = 0; i < nameStart.Length; ++i) { + int start, j = 0; + do { + start = description.IndexOf (nameStart [i], j); + } while (start >= 0 && j != 0 ? description [j++ - 1] == '{' : false); + if (start == -1) + continue; + int end = description.IndexOf ("}", start); + if (end == -1) + continue; + return description.Substring (start + nameStart [i].Length, end - start - nameStart [i].Length); + } + return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); + } + + private static string GetDescription (string description) + { + if (description == null) + return string.Empty; + StringBuilder sb = new StringBuilder (description.Length); + int start = -1; + for (int i = 0; i < description.Length; ++i) { + switch (description [i]) { + case '{': + if (i == start) { + sb.Append ('{'); + start = -1; + } + else if (start < 0) + start = i + 1; + break; + case '}': + if (start < 0) { + if ((i+1) == description.Length || description [i+1] != '}') + throw new InvalidOperationException ("Invalid option description: " + description); + ++i; + sb.Append ("}"); + } + else { + sb.Append (description.Substring (start, i - start)); + start = -1; + } + break; + case ':': + if (start < 0) + goto default; + start = i + 1; + break; + default: + if (start < 0) + sb.Append (description [i]); + break; + } + } + return sb.ToString (); + } + + private static IEnumerable<string> GetLines (string description) + { + if (string.IsNullOrEmpty (description)) { + yield return string.Empty; + yield break; + } + int length = 80 - OptionWidth - 1; + int start = 0, end; + do { + end = GetLineEnd (start, length, description); + char c = description [end-1]; + if (char.IsWhiteSpace (c)) + --end; + bool writeContinuation = end != description.Length && !IsEolChar (c); + string line = description.Substring (start, end - start) + + (writeContinuation ? "-" : ""); + yield return line; + start = end; + if (char.IsWhiteSpace (c)) + ++start; + length = 80 - OptionWidth - 2 - 1; + } while (end < description.Length); + } + + private static bool IsEolChar (char c) + { + return !char.IsLetterOrDigit (c); + } + + private static int GetLineEnd (int start, int length, string description) + { + int end = System.Math.Min (start + length, description.Length); + int sep = -1; + for (int i = start; i < end; ++i) { + if (description [i] == '\n') + return i+1; + if (IsEolChar (description [i])) + sep = i+1; + } + if (sep == -1 || end == description.Length) + return end; + return sep; + } + } +} + diff --git a/SparkleLib/SparklePaths.cs b/SparkleLib/SparklePaths.cs new file mode 100644 index 0000000..3c55097 --- /dev/null +++ b/SparkleLib/SparklePaths.cs @@ -0,0 +1,36 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.IO; + +namespace SparkleLib { + + public static class SparklePaths { + + public static string HomePath = Environment.GetFolderPath (Environment.SpecialFolder.Personal); + public static string SparklePath = Path.Combine (HomePath ,"SparkleShare"); + public static string SparkleTmpPath = Path.Combine (SparklePath, ".tmp"); + public static string SparkleConfigPath = Path.Combine (Environment.GetFolderPath ( + Environment.SpecialFolder.ApplicationData), "sparkleshare"); + public static string SparkleLocalIconPath = Path.Combine (SparkleConfigPath, "icons"); + + public static string SparkleInstallPath = Path.Combine (Defines.PREFIX, "sparkleshare"); + public static string SparkleIconPath = SparkleHelpers.CombineMore (Defines.DATAROOTDIR, "sparkleshare", "icons"); + + } +} diff --git a/SparkleLib/SparkleRepoBase.cs b/SparkleLib/SparkleRepoBase.cs new file mode 100644 index 0000000..cf9d5ac --- /dev/null +++ b/SparkleLib/SparkleRepoBase.cs @@ -0,0 +1,468 @@ +// 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 +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Timers; +using System.Xml; + +namespace SparkleLib { + + public enum SyncStatus { + Idle, + SyncUp, + SyncDown, + Error + } + + + public abstract class SparkleRepoBase { + + + + private Timer local_timer = new Timer () { Interval = 250 }; + private Timer remote_timer = new Timer () { Interval = 60000 }; + private FileSystemWatcher watcher; + private SparkleListenerBase listener; + private List <double> sizebuffer = new List<double> (); + private bool has_changed = false; + private Object change_lock = new Object (); + + protected SyncStatus status; + protected bool is_buffering = false; + protected bool is_polling = true; + protected bool server_online = true; + + public readonly SparkleBackend Backend; + public readonly string LocalPath; + public readonly string Name; + + public abstract bool AnyDifferences { get; } + public abstract string Identifier { get; } + public abstract string CurrentRevision { get; } + public abstract bool SyncUp (); + public abstract bool SyncDown (); + public abstract bool HasUnsyncedChanges { get; set; } + + public delegate void SyncStatusChangedEventHandler (SyncStatus new_status); + public event SyncStatusChangedEventHandler SyncStatusChanged; + + public delegate void NewChangeSetEventHandler (SparkleChangeSet change_set, string source_path); + public event NewChangeSetEventHandler NewChangeSet; + + public delegate void ConflictResolvedEventHandler (); + public event ConflictResolvedEventHandler ConflictResolved; + + public delegate void ChangesDetectedEventHandler (); + public event ChangesDetectedEventHandler ChangesDetected; + + + public SparkleRepoBase (string path, SparkleBackend backend) + { + LocalPath = path; + Name = Path.GetFileName (LocalPath); + Backend = backend; + + SyncStatusChanged += delegate (SyncStatus status) { + this.status = status; + }; + + if (CurrentRevision == null) { + CreateInitialChangeSet (); + HasUnsyncedChanges = true; + } + + CreateWatcher (); + CreateListener (); + + this.local_timer.Elapsed += delegate (object o, ElapsedEventArgs args) { + CheckForChanges (); + }; + + this.remote_timer.Elapsed += delegate { + if (this.is_polling) { + if (CheckForRemoteChanges ()) + SyncDownBase (); + } + + // In the unlikely case that we haven't synced up our + // changes or the server was down, sync up again + if (HasUnsyncedChanges) + SyncUpBase (); + }; + + this.remote_timer.Start (); + this.local_timer.Start (); + + // Sync up everything that changed + // since we've been offline + if (AnyDifferences) { + DisableWatching (); + SyncUpBase (); + + while (HasUnsyncedChanges) + SyncUpBase (); + EnableWatching (); + } + } + + + public bool ServerOnline { + get { + return this.server_online; + } + } + + + public SyncStatus Status { + get { + return this.status; + } + } + + + public string Domain { + get { + Regex regex = new Regex (@"(@|://)([a-z0-9\.-]+)(/|:)"); + Match match = regex.Match (SparkleConfig.DefaultConfig.GetUrlForFolder (Name)); + + if (match.Success) + return match.Groups [2].Value; + else + return null; + } + } + + + protected void OnConflictResolved () + { + HasUnsyncedChanges = true; + + if (ConflictResolved != null) + ConflictResolved (); + } + + + public virtual bool CheckForRemoteChanges () // HasRemoteChanges { get; } ? + { + return true; + } + + + public virtual List<SparkleChangeSet> GetChangeSets (int count) { + return null; + } + + + public virtual bool UsesNotificationCenter { + get { + return true; + } + } + + + public string RemoteName { + get { + string url = SparkleConfig.DefaultConfig.GetUrlForFolder (Name); + return Path.GetFileNameWithoutExtension (url); + } + } + + + public bool IsBuffering { + get { + return this.is_buffering; + } + } + + + public bool IsPolling { + get { + return this.is_polling; + } + } + + + // Disposes all resourses of this object + public void Dispose () + { + this.remote_timer.Dispose (); + this.local_timer.Dispose (); + this.listener.Dispose (); + } + + + private void CreateWatcher () + { + 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); + } + + + private void CreateListener () + { + this.listener = SparkleListenerFactory.CreateIrcListener (Domain, Identifier, + SparkleConfig.DefaultConfig.GetAnnouncementsForFolder (Name)); + + // 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 + if (CheckForRemoteChanges ()) + SyncDownBase (); + + // Push changes that were made since the last disconnect + if (HasUnsyncedChanges) + SyncUpBase (); + }; + + // Start polling when the connection to the irc channel is lost + this.listener.Disconnected += delegate { + SparkleHelpers.DebugInfo (Name, "Falling back to polling"); + this.is_polling = true; + }; + + // Fetch changes when there is a message in the irc channel + this.listener.Announcement += delegate (SparkleAnnouncement announcement) { + string identifier = Identifier; + + if (announcement.FolderIdentifier == identifier && + !announcement.Message.Equals (CurrentRevision)) { + if ((Status != SyncStatus.SyncUp) && + (Status != SyncStatus.SyncDown) && + !this.is_buffering) { + + while (this.listener.HasQueueDownAnnouncement (identifier)) + SyncDownBase (); + } + } + }; + + // Start listening + if (!this.listener.IsConnected && !this.listener.IsConnecting) + this.listener.Connect (); + else + this.is_polling = false; + } + + + 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; + + DisableWatching (); + while (AnyDifferences) + SyncUpBase (); + EnableWatching (); + } + } + } + } + + + // Starts a timer when something changes + public void OnFileActivity (object o, FileSystemEventArgs args) + { + if (args.FullPath.Contains ("/.")) + return; + + WatcherChangeTypes wct = 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) { + if (ChangesDetected != null) + ChangesDetected (); + } + + SparkleHelpers.DebugInfo ("Event", "[" + Name + "] " + wct.ToString () + " '" + args.Name + "'"); + SparkleHelpers.DebugInfo ("Event", "[" + Name + "] Changes found, checking if settled."); + + this.remote_timer.Stop (); + + lock (this.change_lock) { + this.has_changed = true; + } + } + } + + + private void SyncUpBase () + { + try { + this.local_timer.Stop (); + this.remote_timer.Stop (); + + SparkleHelpers.DebugInfo ("SyncUp", "[" + Name + "] Initiated"); + + if (SyncStatusChanged != null) + SyncStatusChanged (SyncStatus.SyncUp); + + if (SyncUp ()) { + SparkleHelpers.DebugInfo ("SyncUp", "[" + Name + "] Done"); + + HasUnsyncedChanges = false; + + if (SyncStatusChanged != null) + SyncStatusChanged (SyncStatus.Idle); + + this.listener.AnnounceBase (new SparkleAnnouncement (Identifier, CurrentRevision)); + + } else { + SparkleHelpers.DebugInfo ("SyncUp", "[" + Name + "] Error"); + + HasUnsyncedChanges = true; + SyncDownBase (); + + if (SyncUp ()) { + HasUnsyncedChanges = false; + + if (SyncStatusChanged != null) + SyncStatusChanged (SyncStatus.Idle); + + this.listener.AnnounceBase (new SparkleAnnouncement (Identifier, CurrentRevision)); + + } else { + if (SyncStatusChanged != null) + SyncStatusChanged (SyncStatus.Error); + } + } + + } finally { + this.remote_timer.Start (); + this.local_timer.Start (); + } + } + + + private void SyncDownBase () + { + SparkleHelpers.DebugInfo ("SyncDown", "[" + Name + "] Initiated"); + this.remote_timer.Stop (); + + if (SyncStatusChanged != null) + SyncStatusChanged (SyncStatus.SyncDown); + + if (SyncDown ()) { + SparkleHelpers.DebugInfo ("SyncDown", "[" + Name + "] Done"); + this.server_online = true; + + if (SyncStatusChanged != null) + SyncStatusChanged (SyncStatus.Idle); + + if (NewChangeSet != null) + NewChangeSet (GetChangeSets (1) [0], LocalPath); + + // There could be changes from a + // resolved conflict. Tries only once, + //then let the timer try again periodicallly + if (HasUnsyncedChanges) + SyncUp (); + + } else { + SparkleHelpers.DebugInfo ("SyncDown", "[" + Name + "] Error"); + this.server_online = false; + + if (SyncStatusChanged != null) + SyncStatusChanged (SyncStatus.Error); + } + + if (SyncStatusChanged != null) + SyncStatusChanged (SyncStatus.Idle); + + this.remote_timer.Start (); + } + + + public void DisableWatching () + { + this.watcher.EnableRaisingEvents = false; + } + + + public void EnableWatching () + { + this.watcher.EnableRaisingEvents = true; + } + + + // Create an initial change set when the + // user has fetched an empty remote folder + public virtual void CreateInitialChangeSet () + { + string file_path = Path.Combine (LocalPath, "SparkleShare.txt"); + TextWriter writer = new StreamWriter (file_path); + writer.WriteLine (":)"); + writer.Close (); + } + + + // 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; + } + } +} |