diff --git a/SparkleShare.sln b/SparkleShare.sln
index 40b22bd55..8b7526e76 100644
--- a/SparkleShare.sln
+++ b/SparkleShare.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SparkleShare.Windows", "Spa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SparkleShare.Linux", "SparkleShare\Linux\SparkleShare.Linux.csproj", "{5714D3CA-88A6-4330-A29D-4CA90D1D193C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sparkles.Subversion", "Sparkles\Subversion\Sparkles.Subversion.csproj", "{009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Release|Any CPU = Release|Any CPU
@@ -48,6 +50,12 @@ Global
{CF5BC8DB-A633-4FCC-8A3E-E3AC9B59FABC}.Release|Any CPU.Build.0 = Release|Any CPU
{CF5BC8DB-A633-4FCC-8A3E-E3AC9B59FABC}.ReleaseDist|Any CPU.ActiveCfg = ReleaseDist|Any CPU
{CF5BC8DB-A633-4FCC-8A3E-E3AC9B59FABC}.ReleaseDist|Any CPU.Build.0 = ReleaseDist|Any CPU
+ {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}.ReleaseDist|Any CPU.ActiveCfg = Release|Any CPU
+ {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}.ReleaseDist|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
Policies = $0
diff --git a/SparkleShare/Common/BaseController.cs b/SparkleShare/Common/BaseController.cs
index 8ca074661..febd5d2ae 100644
--- a/SparkleShare/Common/BaseController.cs
+++ b/SparkleShare/Common/BaseController.cs
@@ -223,6 +223,7 @@ public virtual void Initialize ()
Logger.LogInfo ("Environment", "SparkleShare " + version);
Logger.LogInfo ("Environment", "Git LFS " + Sparkles.Git.GitCommand.GitLFSVersion);
Logger.LogInfo ("Environment", "Git " + Sparkles.Git.GitCommand.GitVersion);
+ Logger.LogInfo ("Environment", "Svn " + Sparkles.Subversion.SubversionCommand.SvnVersion);
// TODO: ToString() with nice os version names (Mac OS X Yosemite, Fedora 24, Ubuntu 16.04, etc.)
Logger.LogInfo ("Environment", InstallationInfo.OperatingSystem + " (" + Environment.OSVersion + ")");
diff --git a/SparkleShare/Common/Images/Sources/Subversion-logo.svg b/SparkleShare/Common/Images/Sources/Subversion-logo.svg
new file mode 100644
index 000000000..b40692206
--- /dev/null
+++ b/SparkleShare/Common/Images/Sources/Subversion-logo.svg
@@ -0,0 +1,34 @@
+
+
+
diff --git a/SparkleShare/Common/Presets/Makefile.am b/SparkleShare/Common/Presets/Makefile.am
index 40efbe968..5bb9f49c3 100644
--- a/SparkleShare/Common/Presets/Makefile.am
+++ b/SparkleShare/Common/Presets/Makefile.am
@@ -4,14 +4,15 @@ dist_presets_DATA = \
gitlab.xml \
own-server.xml \
planio.xml \
+ subversion.xml \
github.png \
gitlab.png \
bitbucket.png \
planio.png \
+ subversion.png \
own-server.png
presetsdir = $(pkgdatadir)/presets/
MAINTAINERCLEANFILES = \
Makefile.in
-
diff --git a/SparkleShare/Common/Presets/subversion.png b/SparkleShare/Common/Presets/subversion.png
new file mode 100644
index 000000000..bd180dc78
Binary files /dev/null and b/SparkleShare/Common/Presets/subversion.png differ
diff --git a/SparkleShare/Common/Presets/subversion.xml b/SparkleShare/Common/Presets/subversion.xml
new file mode 100644
index 000000000..09164a42a
--- /dev/null
+++ b/SparkleShare/Common/Presets/subversion.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ Subversion Server
+ Subversion Server
+ subversion.png
+ Subversion
+
+
+
+ svn://subversion.somewhere.com
+
+
+
+ path/to/folder/you/want/as/root
+
+
+
diff --git a/SparkleShare/Common/Presets/subversion@2x.png b/SparkleShare/Common/Presets/subversion@2x.png
new file mode 100644
index 000000000..2f1ab442a
Binary files /dev/null and b/SparkleShare/Common/Presets/subversion@2x.png differ
diff --git a/SparkleShare/Linux/SparkleShare.Linux.csproj b/SparkleShare/Linux/SparkleShare.Linux.csproj
index f93dc8249..c7d7d06d0 100644
--- a/SparkleShare/Linux/SparkleShare.Linux.csproj
+++ b/SparkleShare/Linux/SparkleShare.Linux.csproj
@@ -49,6 +49,10 @@
{009FDCD7-1D57-4202-BB6D-8477D8C6B8EE}
Sparkles.Git
+
+ {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}
+ Sparkles.Subversion
+
{2C914413-B31C-4362-93C7-1AE34F09112A}
Sparkles
@@ -117,6 +121,15 @@
Presets\planio%402x.png
+
+ Presets\subversion.png
+
+
+ Presets\subversion.xml
+
+
+ Presets\subversion%402x.png
+
@@ -178,4 +191,4 @@
-
\ No newline at end of file
+
diff --git a/SparkleShare/Mac/SparkleShare.Mac.csproj b/SparkleShare/Mac/SparkleShare.Mac.csproj
index 0e1b5cbb6..4cd987aa0 100644
--- a/SparkleShare/Mac/SparkleShare.Mac.csproj
+++ b/SparkleShare/Mac/SparkleShare.Mac.csproj
@@ -56,6 +56,7 @@
false
false
false
+ None
false
@@ -244,6 +245,15 @@
Presets\planio%402x.png
+
+ Presets\subversion.png
+
+
+ Presets\subversion.xml
+
+
+ Presets\subversion%402x.png
+
Resources\text-balloon.png
@@ -278,5 +288,9 @@
{009FDCD7-1D57-4202-BB6D-8477D8C6B8EE}
Sparkles.Git
+
+ {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}
+ Sparkles.Subversion
+
diff --git a/SparkleShare/Windows/SparkleShare.Windows.csproj b/SparkleShare/Windows/SparkleShare.Windows.csproj
index 9a4da88a3..356c7473a 100644
--- a/SparkleShare/Windows/SparkleShare.Windows.csproj
+++ b/SparkleShare/Windows/SparkleShare.Windows.csproj
@@ -215,6 +215,10 @@
Presets\planio.png
Always
+
+ Presets\subversion.png
+ Always
+
@@ -241,6 +245,10 @@
Presets\planio.xml
Always
+
+ Presets\subversion.xml
+ Always
+
diff --git a/Sparkles/BaseFetcher.cs b/Sparkles/BaseFetcher.cs
index 857c16846..e3316c520 100644
--- a/Sparkles/BaseFetcher.cs
+++ b/Sparkles/BaseFetcher.cs
@@ -237,6 +237,10 @@ public static string GetBackend (string address)
return char.ToUpper (backend [0]) + backend.Substring (1);
}
+ if (address.StartsWith ("svn", StringComparison.InvariantCultureIgnoreCase)) {
+ return "Subversion";
+ }
+
return "Git";
}
diff --git a/Sparkles/Command.cs b/Sparkles/Command.cs
index 3bda6b3c7..080b51547 100644
--- a/Sparkles/Command.cs
+++ b/Sparkles/Command.cs
@@ -54,7 +54,7 @@ public Command (string path, string args, bool write_output)
if (!string.IsNullOrEmpty (StartInfo.WorkingDirectory))
folder = Path.GetFileName (StartInfo.WorkingDirectory) + " | ";
-
+
if (write_output)
Logger.LogInfo ("Cmd", folder + Path.GetFileName (StartInfo.FileName) + " " + StartInfo.Arguments);
diff --git a/Sparkles/Subversion/Makefile.am b/Sparkles/Subversion/Makefile.am
new file mode 100755
index 000000000..2c2716f60
--- /dev/null
+++ b/Sparkles/Subversion/Makefile.am
@@ -0,0 +1,21 @@
+ASSEMBLY = Sparkles.Git
+TARGET = library
+
+LINK = -r:$(DIR_BIN)/Sparkles.dll
+
+SOURCES = \
+ SubversionCommand.cs \
+ SubversionFetcher.cs \
+ SubversionRepository.cs
+
+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/Sparkles/Subversion/Sparkles.Subversion.csproj b/Sparkles/Subversion/Sparkles.Subversion.csproj
new file mode 100644
index 000000000..8289ca2f4
--- /dev/null
+++ b/Sparkles/Subversion/Sparkles.Subversion.csproj
@@ -0,0 +1,60 @@
+
+
+
+ Debug
+ AnyCPU
+ 8.0.30703
+ 2.0
+ {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA}
+ Library
+ Properties
+ Sparkles.Subversion
+ Sparkles.Subversion
+ 512
+
+
+ v4.5
+
+
+ True
+ ..\..\bin\
+ prompt
+ 4
+
+
+ False
+ bin\Debug
+ 4
+ TRACE DEBUG
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {2C914413-B31C-4362-93C7-1AE34F09112A}
+ Sparkles
+
+
+
diff --git a/Sparkles/Subversion/SubversionCommand.cs b/Sparkles/Subversion/SubversionCommand.cs
new file mode 100644
index 000000000..bdcad553d
--- /dev/null
+++ b/Sparkles/Subversion/SubversionCommand.cs
@@ -0,0 +1,110 @@
+// SparkleShare, a collaboration and sharing tool.
+// Copyright (C) 2010 Hylke Bons
+// Portions Copyright (C) 2016 Paul Hammant
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser 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 .
+using System;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+namespace Sparkles.Subversion {
+
+ public class SubversionCommand : Command {
+
+ public static string ExecPath;
+
+
+ static string svn_path;
+
+ public static string SvnPath {
+ get {
+ if (svn_path == null)
+ svn_path = LocateCommand ("svn");
+
+ return svn_path;
+ }
+
+ set {
+ svn_path = value;
+ }
+ }
+
+
+ public static string SvnVersion {
+ get {
+ if (SvnPath == null)
+ SvnPath = LocateCommand ("svn");
+
+ var svn_version = new Command (SvnPath, "--version", false);
+
+ if (ExecPath != null)
+ svn_version.SetEnvironmentVariable ("SVN_EXEC_PATH", ExecPath);
+
+ string version = svn_version.StartAndReadStandardOutput ();
+ return version.Substring(0, version.IndexOf("\n")).Replace ("svn, version ", "");
+ }
+ }
+
+ public SubversionCommand (string working_dir, string args) : this (working_dir, args, null)
+ {
+ }
+
+
+ public SubversionCommand (string working_dir, string args, SSHAuthenticationInfo auth_info) : base (SvnPath, args)
+ {
+
+ StartInfo.WorkingDirectory = working_dir;
+
+ if (ExecPath != null)
+ SetEnvironmentVariable ("SVN_EXEC_PATH", ExecPath);
+
+ SetEnvironmentVariable ("LANG", "en_US");
+
+ }
+
+
+ static ErrorStatus FindError (string line)
+ {
+ ErrorStatus error = ErrorStatus.None;
+
+ if (line.Contains ("WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!") ||
+ line.Contains ("WARNING: POSSIBLE DNS SPOOFING DETECTED!")) {
+
+ error = ErrorStatus.HostIdentityChanged;
+ }
+
+ if (line.StartsWith ("Permission denied") ||
+ line.StartsWith ("ssh_exchange_identification: Connection closed by remote host") ||
+ line.StartsWith ("The authenticity of host")) {
+
+ error = ErrorStatus.AuthenticationFailed;
+ }
+
+ if (line.EndsWith ("does not appear to be a git repository"))
+ error = ErrorStatus.NotFound;
+
+ if (line.EndsWith ("expected old/new/ref, got 'shallow"))
+ error = ErrorStatus.IncompatibleClientServer;
+
+ if (line.StartsWith ("error: Disk space exceeded") ||
+ line.EndsWith ("No space left on device") ||
+ line.EndsWith ("file write error (Disk quota exceeded)")) {
+
+ error = ErrorStatus.DiskSpaceExceeded;
+ }
+
+ return error;
+ }
+ }
+}
diff --git a/Sparkles/Subversion/SubversionFetcher.cs b/Sparkles/Subversion/SubversionFetcher.cs
new file mode 100644
index 000000000..16c1bf1e4
--- /dev/null
+++ b/Sparkles/Subversion/SubversionFetcher.cs
@@ -0,0 +1,176 @@
+// SparkleShare, a collaboration and sharing tool.
+// Copyright (C) 2010 Hylke Bons
+// Portions Copyright (C) 2016 Paul Hammant
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser 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 .
+
+
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text.RegularExpressions;
+using System.Threading;
+
+namespace Sparkles.Subversion {
+
+ public class SubversionFetcher : SSHFetcher {
+
+ SubversionCommand svn_checkout;
+ SSHAuthenticationInfo auth_info;
+
+ protected override bool IsFetchedRepoEmpty {
+ get {
+ var svn_rev_parse = new SubversionCommand (TargetFolder, "ls -v -r");
+ svn_rev_parse.StartAndWaitForExit ();
+
+ return (svn_rev_parse.ExitCode != 0);
+ }
+ }
+
+
+ public SubversionFetcher (SparkleFetcherInfo fetcher_info, SSHAuthenticationInfo auth_info) : base (fetcher_info)
+ {
+
+ this.auth_info = auth_info;
+ var uri_builder = new UriBuilder (RemoteUrl);
+
+ uri_builder.Scheme = "svn";
+
+ RemoteUrl = uri_builder.Uri;
+
+ AvailableStorageTypes.Add (
+ new StorageTypeInfo (StorageType.Encrypted, "Encrypted Storage",
+ "Trade off efficiency for privacy;\nencrypts before storing files on the host"));
+ }
+
+
+ public override bool Fetch ()
+ {
+ if (!base.Fetch ())
+ return false;
+
+ StorageType? storage_type = StorageType.Plain;
+
+ if (storage_type == null)
+ return false;
+
+ FetchedRepoStorageType = (StorageType) storage_type;
+
+ string svn_checkout_command = "checkout";
+
+
+ svn_checkout = new SubversionCommand (Configuration.DefaultConfiguration.TmpPath,
+ string.Format ("{0} \"{1}\" \"{2}\"", svn_checkout_command, RemoteUrl, TargetFolder),
+ auth_info);
+
+ svn_checkout.StartInfo.RedirectStandardError = true;
+ svn_checkout.Start ();
+
+ StreamReader output_stream = svn_checkout.StandardError;
+
+ if (FetchedRepoStorageType == StorageType.LargeFiles)
+ output_stream = svn_checkout.StandardOutput;
+
+ double percentage = 0;
+ double speed = 0;
+ string information = "";
+
+ while (!output_stream.EndOfStream) {
+ string line = output_stream.ReadLine ();
+
+ OnProgressChanged (percentage, speed, information);
+ }
+
+ svn_checkout.WaitForExit ();
+
+ if (svn_checkout.ExitCode != 0)
+ return false;
+
+ Thread.Sleep (500);
+ OnProgressChanged (100, 0, "");
+ Thread.Sleep (500);
+
+ return true;
+ }
+
+
+ public override void Stop ()
+ {
+ try {
+ if (svn_checkout != null && !svn_checkout.HasExited) {
+ svn_checkout.Kill ();
+ svn_checkout.Dispose ();
+ }
+
+ } catch (Exception e) {
+ Logger.LogInfo ("Fetcher", "Failed to dispose properly", e);
+ }
+
+ if (Directory.Exists (TargetFolder)) {
+ try {
+ Directory.Delete (TargetFolder, true /* Recursive */ );
+ Logger.LogInfo ("Fetcher", "Deleted '" + TargetFolder + "'");
+
+ } catch (Exception e) {
+ Logger.LogInfo ("Fetcher", "Failed to delete '" + TargetFolder + "'", e);
+ }
+ }
+ }
+
+
+ public override string Complete (StorageType selected_storage_type)
+ {
+ string identifier = base.Complete (selected_storage_type);
+ string identifier_path = Path.Combine (TargetFolder, ".sparkleshare");
+
+ File.WriteAllText (identifier_path, identifier);
+
+ var svn_add = new SubversionCommand (TargetFolder, "add .sparkleshare");
+ var svn_commit = new SubversionCommand (TargetFolder, "commit -m \"Initial commit by SparkleShare\"");
+
+ // We can't do the "commit --all" shortcut because it doesn't add untracked files
+ svn_add.StartAndWaitForExit ();
+ svn_commit.StartAndWaitForExit ();
+
+
+ File.SetAttributes (identifier_path, FileAttributes.Hidden);
+ return identifier;
+ }
+
+
+ public override void EnableFetchedRepoCrypto (string password)
+ {
+ }
+
+
+ public override bool IsFetchedRepoPasswordCorrect (string password)
+ {
+ return true;
+ }
+
+
+ public override string FormatName ()
+ {
+ string name = Path.GetFileName (RemoteUrl.AbsolutePath);
+ name = name.ReplaceUnderscoreWithSpace ();
+
+ if (name.EndsWith (".svn"))
+ name = name.Replace (".svn", "");
+
+ return name;
+ }
+
+
+ }
+}
diff --git a/Sparkles/Subversion/SubversionRepository.cs b/Sparkles/Subversion/SubversionRepository.cs
new file mode 100644
index 000000000..d3eb695df
--- /dev/null
+++ b/Sparkles/Subversion/SubversionRepository.cs
@@ -0,0 +1,701 @@
+// SparkleShare, a collaboration and sharing tool.
+// Copyright (C) 2010 Hylke Bons
+// Portions Copyright (C) 2016 Paul Hammant
+
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser 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 .
+
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+
+namespace Sparkles.Subversion {
+
+ public class SubversionRepository : BaseRepository {
+
+ SSHAuthenticationInfo auth_info;
+
+ bool in_merge {
+ get {
+ string merge_file_path = Path.Combine (LocalPath, ".svn", "MERGE_HEAD");
+ return File.Exists (merge_file_path);
+ }
+ }
+
+
+ public SubversionRepository (string path, Configuration config, SSHAuthenticationInfo auth_info) : base (path, config)
+ {
+ this.auth_info = auth_info;
+ }
+
+
+ public override List ExcludePaths {
+ get {
+ List rules = new List ();
+ rules.Add (".svn");
+
+ return rules;
+ }
+ }
+
+
+ public override double Size {
+ get {
+ // TODO
+ return 0;
+ }
+ }
+
+
+ public override double HistorySize {
+ get {
+ // TODO
+ return 0;
+ }
+ }
+
+
+ public override string CurrentRevision {
+ get {
+ var svn = new SubversionCommand (LocalPath, "info --show-item revision");
+ string output = svn.StartAndReadStandardOutput ();
+
+ if (svn.ExitCode == 0)
+ return output;
+
+ return null;
+ }
+ }
+
+
+ public override bool HasRemoteChanges {
+ get {
+ Logger.LogInfo ("Svn", Name + " | Checking for remote changes...");
+ string current_revision = CurrentRevision;
+
+ var svn = new SubversionCommand (LocalPath,
+ "info --show-item revision " + RemoteUrl, auth_info);
+
+ string output = svn.StartAndReadStandardOutput ();
+
+ if (svn.ExitCode != 0)
+ return false;
+
+ string remote_revision = output;
+
+ if (!remote_revision.Equals (current_revision)) {
+ return true;
+ }
+
+ Logger.LogInfo ("Svn", Name + " | No remote changes, local+remote: " + current_revision);
+ return false;
+ }
+ }
+
+
+ public override bool SyncUp ()
+ {
+ if (!Add ()) {
+ Error = ErrorStatus.UnreadableFiles;
+ return false;
+ }
+
+ string message = base.status_message.Replace ("\"", "\\\"");
+
+ if (string.IsNullOrEmpty (message))
+ message = FormatCommitMessage ();
+
+ var svn_commit = new SubversionCommand (LocalPath, string.Format ("commit -m=\"" + message + "\" ", RemoteUrl), auth_info);
+ svn_commit.StartInfo.RedirectStandardError = true;
+ svn_commit.Start ();
+
+ if (!ReadStream (svn_commit))
+ return false;
+
+ svn_commit.WaitForExit ();
+
+ if (svn_commit.ExitCode == 0)
+ return true;
+
+ Error = ErrorStatus.HostUnreachable;
+ return false;
+ }
+
+
+ public override bool SyncDown ()
+ {
+
+ var svn_up = new SubversionCommand (LocalPath, "up", auth_info);
+
+ svn_up.StartInfo.RedirectStandardError = true;
+ svn_up.Start ();
+
+ if (!ReadStream (svn_up))
+ return false;
+
+ svn_up.WaitForExit ();
+
+ if (svn_up.ExitCode != 0) {
+ Error = ErrorStatus.HostUnreachable;
+ return false;
+ }
+
+ return true;
+ }
+
+
+ bool ReadStream (SubversionCommand command)
+ {
+
+ StreamReader output_stream = command.StandardError;
+
+ string information = "";
+
+ while (!output_stream.EndOfStream) {
+ string line = output_stream.ReadLine ();
+
+ OnProgressChanged (0, 0, information);
+ }
+
+ return true;
+ }
+
+
+ public override bool HasLocalChanges {
+ get {
+ PrepareDirectories (LocalPath);
+
+ var svn = new SubversionCommand (LocalPath, "status");
+ string output = svn.StartAndReadStandardOutput ();
+
+ return !string.IsNullOrEmpty (output);
+ }
+ }
+
+
+ public override bool HasUnsyncedChanges {
+ // TODO
+ get {
+ return false;
+ }
+ set {
+ }
+ }
+
+ // Stages the made changes
+ bool Add ()
+ {
+
+ var svn = new SubversionCommand (LocalPath, "status " + LocalPath);
+ string output = svn.StartAndReadStandardOutput ();
+ int count = 0;
+ using (StringReader reader = new StringReader (output)) {
+ string line;
+ while ((line = reader.ReadLine ()) != null) {
+ if (line.StartsWith ("? ")) {
+ new SubversionCommand (LocalPath, "add " + line.Substring (7)).Start ();
+ count++;
+ } else if (line.StartsWith ("M ")) {
+ // Subversion is going to act on this by default
+ count++;
+ } else if (line.StartsWith ("! ")) {
+ new SubversionCommand (LocalPath, "delete " + line.Substring (7)).Start ();
+ count++;
+ }
+ //TODO see if Add and Delete are on the same item, and do a 'move --force' instead
+ }
+ }
+
+ return count > 0;
+ }
+
+
+ public override void RestoreFile (string path, string revision, string target_file_path)
+ {
+ if (path == null)
+ throw new ArgumentNullException ("path");
+
+ if (revision == null)
+ throw new ArgumentNullException ("revision");
+
+ Logger.LogInfo ("Svn", Name + " | Restoring \"" + path + "\" (revision " + revision + ")");
+
+ // Restore the older file...
+ var svn = new SubversionCommand (LocalPath, "checkout -r" + revision + " \"" + path + "\"");
+ svn.StartAndWaitForExit ();
+
+ string local_file_path = Path.Combine (LocalPath, path);
+
+ // ...move it...
+ try {
+ File.Move (local_file_path, target_file_path);
+
+ } catch {
+ Logger.LogInfo ("Svn",
+ Name + " | Could not move \"" + local_file_path + "\" to \"" + target_file_path + "\"");
+ }
+
+ // ...and restore the most recent revision
+ svn = new SubversionCommand (LocalPath, "checkout -r" + CurrentRevision + " \"" + path + "\"");
+ svn.StartAndWaitForExit ();
+
+
+ if (target_file_path.StartsWith (LocalPath))
+ new Thread (() => OnFileActivity (null)).Start ();
+ }
+
+
+ public override List UnsyncedChanges {
+ get {
+ return ParseStatus ();
+ }
+ }
+
+
+ public override List GetChangeSets ()
+ {
+ return GetChangeSetsInternal (null);
+ }
+
+ public override List GetChangeSets (string path)
+ {
+ return GetChangeSetsInternal (path);
+ }
+
+ List GetChangeSetsInternal (string path)
+ {
+ var change_sets = new List ();
+ SubversionCommand svn;
+
+ svn = new SubversionCommand (LocalPath, "log -l 100");
+
+ string output = svn.StartAndReadStandardOutput ();
+
+ string [] lines = output.Split ("\n".ToCharArray ());
+ List entries = new List ();
+
+ // Split up commit entries
+ int line_number = 0;
+ string entry = "";
+
+ while (lines.Length <= line_number) {
+ if (lines [line_number].StartsWith ("-------")) {
+ if (lines [line_number + 1].StartsWith ("r")) {
+ entry = lines [line_number + 1];
+ entry = entry + " <###> " + lines [line_number + 3];
+ line_number += 3;
+ entries.Add (entry);
+ }
+ line_number += 1;
+ }
+ }
+
+ // Parse commit entries
+ foreach (string log_entry in entries) {
+ Match match = this.log_regex.Match (log_entry);
+
+ if (!match.Success) {
+ match = this.merge_regex.Match (log_entry);
+
+ if (!match.Success)
+ continue;
+ }
+
+ ChangeSet change_set = new ChangeSet ();
+
+ change_set.Folder = new SparkleFolder (Name);
+ change_set.Revision = match.Groups [1].Value;
+ change_set.User = new User (match.Groups [2].Value, match.Groups [3].Value);
+ change_set.RemoteUrl = RemoteUrl;
+
+ 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 (0, 3));
+ change_set.Timestamp = change_set.Timestamp.AddHours (their_offset * -1);
+ change_set.Timestamp = change_set.Timestamp.AddHours (our_offset);
+
+ string [] entry_lines = log_entry.Split ("\n".ToCharArray ());
+
+ // Parse file list. Lines containing file changes start with ":"
+ foreach (string entry_line in entry_lines) {
+ // Skip lines containing backspace characters
+ if (!entry_line.StartsWith (":") || entry_line.Contains ("\\177"))
+ continue;
+
+ string file_path = entry_line.Substring (39);
+
+ if (file_path.Equals (".sparkleshare"))
+ continue;
+
+ string type_letter = entry_line [37].ToString ();
+ bool change_is_folder = false;
+
+ if (file_path.EndsWith (".empty")) {
+ file_path = file_path.Substring (0, file_path.Length - ".empty".Length);
+ change_is_folder = true;
+ }
+
+ try {
+ file_path = EnsureSpecialCharacters (file_path);
+
+ } catch (Exception e) {
+ Logger.LogInfo ("Local", "Error parsing file name '" + file_path + "'", e);
+ continue;
+ }
+
+ file_path = file_path.Replace ("\\\"", "\"");
+
+ Change change = new Change () {
+ Path = file_path,
+ IsFolder = change_is_folder,
+ Timestamp = change_set.Timestamp,
+ Type = ChangeType.Added
+ };
+
+ if (type_letter.Equals ("R")) {
+ int tab_pos = entry_line.LastIndexOf ("\t");
+ file_path = entry_line.Substring (42, tab_pos - 42);
+ string to_file_path = entry_line.Substring (tab_pos + 1);
+
+ try {
+ file_path = EnsureSpecialCharacters (file_path);
+
+ } catch (Exception e) {
+ Logger.LogInfo ("Local", "Error parsing file name '" + file_path + "'", e);
+ continue;
+ }
+
+ try {
+ to_file_path = EnsureSpecialCharacters (to_file_path);
+
+ } catch (Exception e) {
+ Logger.LogInfo ("Local", "Error parsing file name '" + to_file_path + "'", e);
+ continue;
+ }
+
+ file_path = file_path.Replace ("\\\"", "\"");
+ to_file_path = to_file_path.Replace ("\\\"", "\"");
+
+ if (file_path.EndsWith (".empty")) {
+ file_path = file_path.Substring (0, file_path.Length - 6);
+ change_is_folder = true;
+ }
+
+ if (to_file_path.EndsWith (".empty")) {
+ to_file_path = to_file_path.Substring (0, to_file_path.Length - 6);
+ change_is_folder = true;
+ }
+
+ change.Path = file_path;
+ change.MovedToPath = to_file_path;
+ change.Type = ChangeType.Moved;
+
+ } else if (type_letter.Equals ("M")) {
+ change.Type = ChangeType.Edited;
+
+ } else if (type_letter.Equals ("D")) {
+ change.Type = ChangeType.Deleted;
+ }
+
+ change_set.Changes.Add (change);
+ }
+
+ // Group commits per user, per day
+ if (change_sets.Count > 0 && path == null) {
+ ChangeSet last_change_set = change_sets [change_sets.Count - 1];
+
+ if (change_set.Timestamp.Year == last_change_set.Timestamp.Year &&
+ change_set.Timestamp.Month == last_change_set.Timestamp.Month &&
+ change_set.Timestamp.Day == last_change_set.Timestamp.Day &&
+ change_set.User.Name.Equals (last_change_set.User.Name)) {
+
+ last_change_set.Changes.AddRange (change_set.Changes);
+
+ if (DateTime.Compare (last_change_set.Timestamp, change_set.Timestamp) < 1) {
+ last_change_set.FirstTimestamp = last_change_set.Timestamp;
+ last_change_set.Timestamp = change_set.Timestamp;
+ last_change_set.Revision = change_set.Revision;
+
+ } else {
+ last_change_set.FirstTimestamp = change_set.Timestamp;
+ }
+
+ } else {
+ change_sets.Add (change_set);
+ }
+
+ } else {
+ // Don't show removals or moves in the revision list of a file
+ if (path != null) {
+ List changes_to_skip = new List ();
+
+ foreach (Change change in change_set.Changes) {
+ if ((change.Type == ChangeType.Deleted || change.Type == ChangeType.Moved)
+ && change.Path.Equals (path)) {
+
+ changes_to_skip.Add (change);
+ }
+ }
+
+ foreach (Change change_to_skip in changes_to_skip)
+ change_set.Changes.Remove (change_to_skip);
+ }
+
+ change_sets.Add (change_set);
+ }
+ }
+
+ return change_sets;
+ }
+
+
+ string EnsureSpecialCharacters (string path)
+ {
+ // The path is quoted if it contains special characters
+ if (path.StartsWith ("\""))
+ path = ResolveSpecialChars (path.Substring (1, path.Length - 2));
+
+ return path;
+ }
+
+
+ string ResolveSpecialChars (string s)
+ {
+ StringBuilder builder = new StringBuilder (s.Length);
+ List codes = new List ();
+
+ for (int i = 0; i < s.Length; i++) {
+ while (s [i] == '\\' &&
+ s.Length - i > 3 &&
+ char.IsNumber (s [i + 1]) &&
+ char.IsNumber (s [i + 2]) &&
+ char.IsNumber (s [i + 3])) {
+
+ codes.Add (Convert.ToByte (s.Substring (i + 1, 3), 8));
+ i += 4;
+ }
+
+ if (codes.Count > 0) {
+ builder.Append (Encoding.UTF8.GetString (codes.ToArray ()));
+ codes.Clear ();
+ }
+
+ builder.Append (s [i]);
+ }
+
+ return builder.ToString ();
+ }
+
+
+ // Git doesn't track empty directories, so this method
+ // fills them all with a hidden empty file.
+ //
+ // It also prevents git repositories from becoming
+ // git submodules by renaming the .git/HEAD file
+ void PrepareDirectories (string path)
+ {
+ try {
+ foreach (string child_path in Directory.GetDirectories (path)) {
+ if (IsSymlink (child_path))
+ continue;
+
+ if (child_path.EndsWith (".svn")) {
+ if (child_path.Equals (Path.Combine (LocalPath, ".svn")))
+ continue;
+
+ string HEAD_file_path = Path.Combine (child_path, "HEAD");
+
+ if (File.Exists (HEAD_file_path)) {
+ File.Move (HEAD_file_path, HEAD_file_path + ".backup");
+ Logger.LogInfo ("Svn", Name + " | Renamed " + HEAD_file_path);
+ }
+
+ continue;
+ }
+
+ PrepareDirectories (child_path);
+ }
+
+ if (Directory.GetFiles (path).Length == 0 &&
+ Directory.GetDirectories (path).Length == 0 &&
+ !path.Equals (LocalPath)) {
+
+ if (!File.Exists (Path.Combine (path, ".empty"))) {
+ try {
+ File.WriteAllText (Path.Combine (path, ".empty"), "I'm a folder!");
+ File.SetAttributes (Path.Combine (path, ".empty"), FileAttributes.Hidden);
+
+ } catch {
+ Logger.LogInfo ("Svn", Name + " | Failed adding empty folder " + path);
+ }
+ }
+ }
+
+ } catch (IOException e) {
+ Logger.LogInfo ("Svn", "Failed preparing directory", e);
+ }
+ }
+
+
+ List ParseStatus ()
+ {
+ List changes = new List ();
+
+ var svn_status = new SubversionCommand (LocalPath, "status " + LocalPath);
+ svn_status.Start ();
+
+ while (!svn_status.StandardOutput.EndOfStream) {
+ string line = svn_status.StandardOutput.ReadLine ();
+
+ line = line.Trim ();
+ Console.WriteLine ("LL " + line);
+
+ Change change;
+
+ if (line.StartsWith ("R")) {
+ string path = line.Substring (3, line.IndexOf (" -> ") - 3).Trim ("\" ".ToCharArray ());
+ string moved_to_path = line.Substring (line.IndexOf (" -> ") + 4).Trim ("\" ".ToCharArray ());
+
+ change = new Change () {
+ Type = ChangeType.Moved,
+ Path = EnsureSpecialCharacters (path),
+ MovedToPath = EnsureSpecialCharacters (moved_to_path)
+ };
+
+ } else {
+ string path = line.Substring (2).Trim ("\" ".ToCharArray ());
+ change = new Change () { Path = EnsureSpecialCharacters (path) };
+ change.Type = ChangeType.Added;
+
+ if (line.StartsWith ("M")) {
+ change.Type = ChangeType.Edited;
+
+ } else if (line.StartsWith ("D")) {
+ change.Type = ChangeType.Deleted;
+ }
+ }
+
+ changes.Add (change);
+ }
+
+ svn_status.StandardOutput.ReadToEnd ();
+ svn_status.WaitForExit ();
+
+ return changes;
+ }
+
+
+ // Creates a pretty commit message based on what has changed
+ string FormatCommitMessage ()
+ {
+ string message = "";
+
+ foreach (Change change in ParseStatus ()) {
+ if (change.Type == ChangeType.Moved) {
+ message += "< ‘" + EnsureSpecialCharacters (change.Path) + "’\n";
+ message += "> ‘" + EnsureSpecialCharacters (change.MovedToPath) + "’\n";
+
+ } else {
+ switch (change.Type) {
+ case ChangeType.Edited:
+ message += "/";
+ break;
+ case ChangeType.Deleted:
+ message += "-";
+ break;
+ case ChangeType.Added:
+ message += "+";
+ break;
+ }
+
+ message += " ‘" + change.Path + "’\n";
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace (message))
+ return null;
+ else
+ return message;
+ }
+
+
+ // Recursively gets a folder's size in bytes
+ long CalculateSizes (DirectoryInfo parent)
+ {
+ long size = 0;
+
+ try {
+ foreach (DirectoryInfo directory in parent.GetDirectories ()) {
+ if (directory.FullName.IsSymlink () ||
+ directory.Name.Equals (".svn")) {
+
+ continue;
+ }
+
+ size += CalculateSizes (directory);
+ }
+
+ } catch (Exception e) {
+ Logger.LogInfo ("Local", "Error calculating directory size", e);
+ }
+
+ try {
+ foreach (FileInfo file in parent.GetFiles ()) {
+ if (file.FullName.IsSymlink ())
+ continue;
+
+ if (file.Name.Equals (".empty"))
+ File.SetAttributes (file.FullName, FileAttributes.Hidden);
+ else
+ size += file.Length;
+ }
+
+ } catch (Exception e) {
+ Logger.LogInfo ("Local", "Error calculating file size", e);
+ }
+
+ return size;
+ }
+
+
+ bool IsSymlink (string file)
+ {
+ FileAttributes attributes = File.GetAttributes (file);
+ return ((attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint);
+ }
+
+
+ Regex log_regex = new Regex (@"commit ([a-f0-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);
+
+ Regex merge_regex = new Regex (@"commit ([a-f0-9]{40})\n" +
+ "Merge: [a-f0-9]{7} [a-f0-9]{7}\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);
+ }
+}