summaryrefslogtreecommitdiff
path: root/SparkleLib
diff options
context:
space:
mode:
authorIain Lane <laney@debian.org>2011-06-15 16:22:22 +0100
committerIain Lane <laney@debian.org>2011-06-15 16:22:22 +0100
commit040fed94f0bcc4e9966ab31cc68d31fa48c27ee3 (patch)
tree808bd5ff78033ca1c23b8df80f55dc544aed98b7 /SparkleLib
Imported Upstream version 0.2.1
Diffstat (limited to 'SparkleLib')
-rw-r--r--SparkleLib/AssemblyInfo.cs11
-rw-r--r--SparkleLib/AssemblyInfo.cs.in11
-rw-r--r--SparkleLib/Defines.cs30
-rw-r--r--SparkleLib/Defines.cs.in30
-rw-r--r--SparkleLib/Git/SparkleFetcherGit.cs195
-rw-r--r--SparkleLib/Git/SparkleRepoGit.cs556
-rw-r--r--SparkleLib/Hg/SparkleFetcherHg.cs172
-rw-r--r--SparkleLib/Hg/SparkleRepoHg.cs319
-rw-r--r--SparkleLib/Makefile.am41
-rw-r--r--SparkleLib/Makefile.in606
-rw-r--r--SparkleLib/Scp/SparkleFetcherScp.cs139
-rw-r--r--SparkleLib/Scp/SparkleRepoScp.cs115
-rw-r--r--SparkleLib/SparkleBackend.cs125
-rw-r--r--SparkleLib/SparkleChangeSet.cs37
-rw-r--r--SparkleLib/SparkleConfig.cs258
-rw-r--r--SparkleLib/SparkleFetcherBase.cs199
-rw-r--r--SparkleLib/SparkleHelpers.cs80
-rw-r--r--SparkleLib/SparkleListenerBase.cs201
-rw-r--r--SparkleLib/SparkleListenerIrc.cs160
-rw-r--r--SparkleLib/SparkleOptions.cs1101
-rw-r--r--SparkleLib/SparklePaths.cs36
-rw-r--r--SparkleLib/SparkleRepoBase.cs468
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;
+ }
+ }
+}