From 6a36031a60f089fd53649f91d2e5691f00ebcc25 Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Sun, 21 Aug 2016 15:16:01 -0400 Subject: [PATCH 1/6] font not available on El Capitan --- SparkleShare/Mac/UserInterface/UserInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SparkleShare/Mac/UserInterface/UserInterface.cs b/SparkleShare/Mac/UserInterface/UserInterface.cs index e53e472b1..81ce11ea4 100755 --- a/SparkleShare/Mac/UserInterface/UserInterface.cs +++ b/SparkleShare/Mac/UserInterface/UserInterface.cs @@ -60,7 +60,7 @@ public static string FontName { if (Environment.OSVersion.Version.Major < 14) return "Lucida Grande"; - if (Environment.OSVersion.Version.Major < 15) + if (Environment.OSVersion.Version.Major <= 15) return "Helvetica Neue"; return "SF UI Text"; From 1ddb3608a872629170ee237fde532e86dc14f00d Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Sun, 21 Aug 2016 15:56:15 -0400 Subject: [PATCH 2/6] start of Subversion work --- SparkleShare.sln | 8 + .../Common/Images/Sources/Subversion-logo.svg | 34 + SparkleShare/Common/Presets/Makefile.am | 3 +- SparkleShare/Common/Presets/subversion.png | Bin 0 -> 4977 bytes SparkleShare/Common/Presets/subversion.xml | 19 + SparkleShare/Common/Presets/subversion@2x.png | Bin 0 -> 7549 bytes SparkleShare/Linux/SparkleShare.Linux.csproj | 11 +- SparkleShare/Mac/SparkleShare.Mac.csproj | 10 + .../Windows/SparkleShare.Windows.csproj | 8 + Sparkles/Subversion/Makefile.am | 21 + .../Subversion/Sparkles.Subversion.csproj | 60 + Sparkles/Subversion/SubversionCommand.cs | 224 ++++ Sparkles/Subversion/SubversionFetcher.cs | 437 +++++++ Sparkles/Subversion/SubversionRepository.cs | 1081 +++++++++++++++++ 14 files changed, 1914 insertions(+), 2 deletions(-) create mode 100644 SparkleShare/Common/Images/Sources/Subversion-logo.svg create mode 100644 SparkleShare/Common/Presets/subversion.png create mode 100644 SparkleShare/Common/Presets/subversion.xml create mode 100644 SparkleShare/Common/Presets/subversion@2x.png create mode 100755 Sparkles/Subversion/Makefile.am create mode 100644 Sparkles/Subversion/Sparkles.Subversion.csproj create mode 100644 Sparkles/Subversion/SubversionCommand.cs create mode 100644 Sparkles/Subversion/SubversionFetcher.cs create mode 100644 Sparkles/Subversion/SubversionRepository.cs 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/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 @@ + + +image/svg+xmlsubversionlogosubversion logoSept 2010Franziska SponselFranziska SponselRRZEHendrik Eggers, Beate Kaspar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..bd180dc78be1b0df8f4e5d8dcd947fdb8d8d5583 GIT binary patch literal 4977 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2f1ab442a434201c105966db2acefa61f32042f3 GIT binary patch literal 7549 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 Qzjq(1ikb=)a#o@L2QgwtzyJUM literal 0 HcmV?d00001 diff --git a/SparkleShare/Linux/SparkleShare.Linux.csproj b/SparkleShare/Linux/SparkleShare.Linux.csproj index f93dc8249..d988170c9 100644 --- a/SparkleShare/Linux/SparkleShare.Linux.csproj +++ b/SparkleShare/Linux/SparkleShare.Linux.csproj @@ -117,6 +117,15 @@ Presets\planio%402x.png + + Presets\subversion.png + + + Presets\subversion.xml + + + Presets\subversion%402x.png + @@ -178,4 +187,4 @@ - \ No newline at end of file + diff --git a/SparkleShare/Mac/SparkleShare.Mac.csproj b/SparkleShare/Mac/SparkleShare.Mac.csproj index 0e1b5cbb6..3db3e27be 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 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/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..809365176 --- /dev/null +++ b/Sparkles/Subversion/SubversionCommand.cs @@ -0,0 +1,224 @@ +// SparkleShare, a collaboration and sharing tool. +// Copyright (C) 2010 Hylke Bons +// +// 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 SSHPath = "ssh"; + public static string ExecPath; + + + static string git_path; + + public static string GitPath { + get { + if (git_path == null) + git_path = LocateCommand ("svn"); + + return git_path; + } + + set { + git_path = value; + } + } + + + public static string GitVersion { + get { + if (GitPath == null) + GitPath = LocateCommand ("svn"); + + var git_version = new Command (GitPath, "--version", false); + + if (ExecPath != null) + git_version.SetEnvironmentVariable ("GIT_EXEC_PATH", ExecPath); + + string version = git_version.StartAndReadStandardOutput (); + return version.Replace ("svn version ", ""); + } + } + + + public static string GitLFSVersion { + get { + if (GitPath == null) + GitPath = LocateCommand ("git"); + + var git_lfs_version = new Command (GitPath, "lfs version", false); + + if (ExecPath != null) + git_lfs_version.SetEnvironmentVariable ("GIT_EXEC_PATH", ExecPath); + + string version = git_lfs_version.StartAndReadStandardOutput (); + return version.Replace ("git-lfs/", "").Split (' ') [0]; + } + } + + + public SubversionCommand (string working_dir, string args) : this (working_dir, args, null) + { + } + + + public SubversionCommand (string working_dir, string args, SSHAuthenticationInfo auth_info) : base (GitPath, args) + { + StartInfo.WorkingDirectory = working_dir; + + string GIT_SSH_COMMAND = SSHPath; + + if (auth_info != null) + GIT_SSH_COMMAND = FormatGitSSHCommand (auth_info); + + if (ExecPath != null) + SetEnvironmentVariable ("GIT_EXEC_PATH", ExecPath); + + SetEnvironmentVariable ("GIT_SSH_COMMAND", GIT_SSH_COMMAND); + SetEnvironmentVariable ("GIT_TERMINAL_PROMPT", "0"); + SetEnvironmentVariable ("LANG", "en_US"); + } + + + static Regex progress_regex = new Regex (@"([0-9]+)%", RegexOptions.Compiled); + static Regex progress_regex_lfs = new Regex (@".*\(([0-9]+) of ([0-9]+) files\).*", RegexOptions.Compiled); + static Regex progress_regex_lfs_skipped = new Regex (@".*\(([0-9]+) of ([0-9]+) files, ([0-9]+) skipped\).*", RegexOptions.Compiled); + static Regex speed_regex = new Regex (@"([0-9\.]+) ([KM])iB/s", RegexOptions.Compiled); + + public static ErrorStatus ParseProgress (string line, out double percentage, out double speed, out string information) + { + percentage = 0; + speed = 0; + information = ""; + + Match match; + + if (line.StartsWith ("Git LFS:")) { + match = progress_regex_lfs_skipped.Match (line); + + int current_file = 0; + int total_file_count = 0; + int skipped_file_count = 0; + + if (match.Success) { + // "skipped" files are objects that have already been transferred + skipped_file_count = int.Parse (match.Groups [3].Value); + + } else { + + match = progress_regex_lfs.Match (line); + + if (!match.Success) + return ErrorStatus.None; + } + + current_file = int.Parse (match.Groups [1].Value); + + if (current_file == 0) + return ErrorStatus.None; + + total_file_count = int.Parse (match.Groups [2].Value) - skipped_file_count; + + percentage = Math.Round ((double) current_file / total_file_count * 100, 0); + information = string.Format ("{0} of {1} files", current_file, total_file_count); + + return ErrorStatus.None; + } + + match = progress_regex.Match (line); + + if (!match.Success || string.IsNullOrWhiteSpace (line)) { + if (!string.IsNullOrWhiteSpace (line)) + Logger.LogInfo ("Git", line); + + return FindError (line); + } + + int number = int.Parse (match.Groups [1].Value); + + // The transfer process consists of two stages: the "Compressing + // objects" stage which we count as 20% of the total progress, and + // the "Writing objects" stage which we count as the last 80% + if (line.Contains ("Compressing objects")) { + // "Compressing objects" stage + percentage = (number / 100 * 20); + + } else if (line.Contains ("Writing objects")) { + percentage = (number / 100 * 80 + 20); + Match speed_match = speed_regex.Match (line); + + if (speed_match.Success) { + speed = double.Parse (speed_match.Groups [1].Value, new CultureInfo ("en-US")) * 1024; + + if (speed_match.Groups [2].Value.Equals ("M")) + speed = speed * 1024; + + information = speed.ToSize (); + } + } + + return ErrorStatus.None; + } + + + 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; + } + + + public static string FormatGitSSHCommand (SSHAuthenticationInfo auth_info) + { + return SSHPath + " " + + "-i " + auth_info.PrivateKeyFilePath.Replace (" ", "\\ ") + " " + + "-o UserKnownHostsFile=" + auth_info.KnownHostsFilePath.Replace (" ", "\\ ") + " " + + "-o IdentitiesOnly=yes" + " " + // Don't fall back to other keys on the system + "-o PasswordAuthentication=no" + " " + // Don't hang on possible password prompts + "-F /dev/null"; // Ignore the system's SSH config file + } + } +} diff --git a/Sparkles/Subversion/SubversionFetcher.cs b/Sparkles/Subversion/SubversionFetcher.cs new file mode 100644 index 000000000..8343d09e5 --- /dev/null +++ b/Sparkles/Subversion/SubversionFetcher.cs @@ -0,0 +1,437 @@ +// SparkleShare, a collaboration and sharing tool. +// Copyright (C) 2010 Hylke Bons +// +// 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 GitFetcher : SSHFetcher { + + SubversionCommand git_clone; + SSHAuthenticationInfo auth_info; + + string password_salt = Path.GetRandomFileName ().SHA256 ().Substring (0, 16); + + + protected override bool IsFetchedRepoEmpty { + get { + var git_rev_parse = new SubversionCommand (TargetFolder, "rev-parse HEAD"); + git_rev_parse.StartAndWaitForExit (); + + return (git_rev_parse.ExitCode != 0); + } + } + + + public GitFetcher (SparkleFetcherInfo fetcher_info, SSHAuthenticationInfo auth_info) : base (fetcher_info) + { + this.auth_info = auth_info; + var uri_builder = new UriBuilder (RemoteUrl); + + if (!RemoteUrl.Scheme.Equals ("ssh") && !RemoteUrl.Scheme.Equals ("git")) + uri_builder.Scheme = "ssh"; + + if (RemoteUrl.Host.Equals ("github.com") || + RemoteUrl.Host.Equals ("gitlab.com")) { + + AvailableStorageTypes.Add ( + new StorageTypeInfo (StorageType.LargeFiles, "Large File Storage", + "Trade off versioning to save space;\nkeeps file history on the host only")); + + uri_builder.Scheme = "ssh"; + uri_builder.UserName = "git"; + + if (!RemoteUrl.AbsolutePath.EndsWith (".git")) + uri_builder.Path += ".git"; + + } else if (string.IsNullOrEmpty (RemoteUrl.UserInfo)) { + uri_builder.UserName = "storage"; + } + + 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 = DetermineStorageType (); + + if (storage_type == null) + return false; + + FetchedRepoStorageType = (StorageType) storage_type; + + string git_clone_command = "clone --progress --no-checkout"; + + if (!FetchPriorHistory) + git_clone_command += " --depth=1"; + + if (storage_type == StorageType.LargeFiles) + git_clone_command = "lfs clone --progress --no-checkout"; + + git_clone = new SubversionCommand (Configuration.DefaultConfiguration.TmpPath, + string.Format ("{0} \"{1}\" \"{2}\"", git_clone_command, RemoteUrl, TargetFolder), + auth_info); + + git_clone.StartInfo.RedirectStandardError = true; + git_clone.Start (); + + StreamReader output_stream = git_clone.StandardError; + + if (FetchedRepoStorageType == StorageType.LargeFiles) + output_stream = git_clone.StandardOutput; + + double percentage = 0; + double speed = 0; + string information = ""; + + while (!output_stream.EndOfStream) { + string line = output_stream.ReadLine (); + + ErrorStatus error = SubversionCommand.ParseProgress (line, out percentage, out speed, out information); + + if (error != ErrorStatus.None) { + IsActive = false; + git_clone.Kill (); + git_clone.Dispose (); + + return false; + } + + OnProgressChanged (percentage, speed, information); + } + + git_clone.WaitForExit (); + + if (git_clone.ExitCode != 0) + return false; + + Thread.Sleep (500); + OnProgressChanged (100, 0, ""); + Thread.Sleep (500); + + return true; + } + + + public override void Stop () + { + try { + if (git_clone != null && !git_clone.HasExited) { + git_clone.Kill (); + git_clone.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"); + + InstallConfiguration (); + InstallGitLFS (); + + InstallAttributeRules (); + InstallExcludeRules (); + + if (IsFetchedRepoEmpty) { + File.WriteAllText (identifier_path, identifier); + + var git_add = new SubversionCommand (TargetFolder, "add .sparkleshare"); + var git_commit = new SubversionCommand (TargetFolder, "commit --message=\"Initial commit by SparkleShare\""); + + // We can't do the "commit --all" shortcut because it doesn't add untracked files + git_add.StartAndWaitForExit (); + git_commit.StartAndWaitForExit (); + + // These branches will be pushed later by "git push --all" + if (selected_storage_type == StorageType.LargeFiles) { + var git_branch = new SubversionCommand (TargetFolder, "branch x-sparkleshare-lfs", auth_info); + git_branch.StartAndWaitForExit (); + } + + if (selected_storage_type == StorageType.Encrypted) { + var git_branch = new SubversionCommand (TargetFolder, + string.Format ("branch x-sparkleshare-encrypted-{0}", password_salt), auth_info); + + git_branch.StartAndWaitForExit (); + } + + } else { + if (File.Exists (identifier_path)) + identifier = File.ReadAllText (identifier_path).Trim (); + + string branch = "HEAD"; + string prefered_branch = "SparkleShare"; + + // Prefer the "SparkleShare" branch if it exists + var git_show_ref = new SubversionCommand (TargetFolder, + "show-ref --verify --quiet refs/heads/" + prefered_branch); + + git_show_ref.StartAndWaitForExit (); + + if (git_show_ref.ExitCode == 0) + branch = prefered_branch; + + var git_checkout = new SubversionCommand (TargetFolder, string.Format ("checkout --quiet --force {0}", branch)); + git_checkout.StartAndWaitForExit (); + } + + // git-lfs may leave junk behind + string git_lfs_tmp_path = Path.Combine (Configuration.DefaultConfiguration.TmpPath, "lfs"); + + if (Directory.Exists (git_lfs_tmp_path)) + Directory.Delete (git_lfs_tmp_path, true); + + File.SetAttributes (identifier_path, FileAttributes.Hidden); + return identifier; + } + + + public override void EnableFetchedRepoCrypto (string password) + { + string password_file = ".git/info/encryption_password"; + var git_config_required = new SubversionCommand (TargetFolder, "config filter.encryption.required true"); + + var git_config_smudge = new SubversionCommand (TargetFolder, "config filter.encryption.smudge " + + string.Format ("\"openssl enc -d -aes-256-cbc -base64 -S {0} -pass file:{1}\"", password_salt, password_file)); + + var git_config_clean = new SubversionCommand (TargetFolder, "config filter.encryption.clean " + + string.Format ("\"openssl enc -e -aes-256-cbc -base64 -S {0} -pass file:{1}\"", password_salt, password_file)); + + git_config_required.StartAndWaitForExit (); + git_config_smudge.StartAndWaitForExit (); + git_config_clean.StartAndWaitForExit (); + + // Store the password, TODO: 600 permissions + string password_file_path = Path.Combine (TargetFolder, ".git", "info", "encryption_password"); + File.WriteAllText (password_file_path, password.SHA256 (password_salt)); + } + + + public override bool IsFetchedRepoPasswordCorrect (string password) + { + string password_check_file_path = Path.Combine (TargetFolder, ".sparkleshare"); + + if (!File.Exists (password_check_file_path)) { + var git_show = new SubversionCommand (TargetFolder, "show HEAD:.sparkleshare"); + string output = git_show.StartAndReadStandardOutput (); + + if (git_show.ExitCode == 0) + File.WriteAllText (password_check_file_path, output); + else + return false; + } + + string args = string.Format ("enc -d -aes-256-cbc -base64 -S {0} -pass pass:{1} -in \"{2}\"", + password_salt, password.SHA256 (password_salt), password_check_file_path); + + var process = new Command ("openssl", args); + + process.StartInfo.WorkingDirectory = TargetFolder; + process.StartAndWaitForExit (); + + if (process.ExitCode == 0) { + File.Delete (password_check_file_path); + return true; + } + + return false; + } + + + public override string FormatName () + { + string name = Path.GetFileName (RemoteUrl.AbsolutePath); + name = name.ReplaceUnderscoreWithSpace (); + + if (name.EndsWith (".git")) + name = name.Replace (".git", ""); + + return name; + } + + + StorageType? DetermineStorageType () + { + var git_ls_remote = new SubversionCommand (Configuration.DefaultConfiguration.TmpPath, + string.Format ("ls-remote --heads \"{0}\"", RemoteUrl), auth_info); + + string output = git_ls_remote.StartAndReadStandardOutput (); + + if (git_ls_remote.ExitCode != 0) + return null; + + if (string.IsNullOrWhiteSpace (output)) + return StorageType.Unknown; + + foreach (string line in output.Split ("\n".ToCharArray ())) { + string [] line_parts = line.Split ('/'); + string branch = line_parts [line_parts.Length - 1]; + + if (branch == "x-sparkleshare-lfs") + return StorageType.LargeFiles; + + string encrypted_storage_prefix = "x-sparkleshare-encrypted-"; + + if (branch.StartsWith (encrypted_storage_prefix)) { + password_salt = branch.Replace (encrypted_storage_prefix, ""); + return StorageType.Encrypted; + } + } + + return StorageType.Plain; + } + + + void InstallConfiguration () + { + string [] settings = { + "core.autocrlf input", + "core.quotepath false", // Don't quote "unusual" characters in path names + "core.ignorecase false", // Be case sensitive explicitly to work on Mac + "core.filemode false", // Ignore permission changes + "core.precomposeunicode true", // Use the same Unicode form on all filesystems + "core.safecrlf false", + "core.excludesfile \"\"", + "core.packedGitLimit 128m", // Some memory limiting options + "core.packedGitWindowSize 128m", + "pack.deltaCacheSize 128m", + "pack.packSizeLimit 128m", + "pack.windowMemory 128m", + "push.default matching" + }; + + if (InstallationInfo.OperatingSystem == OS.Windows) + settings [0] = "core.autocrlf true"; + + foreach (string setting in settings) { + var git_config = new SubversionCommand (TargetFolder, "config " + setting); + git_config.StartAndWaitForExit (); + } + } + + + void InstallExcludeRules () + { + string git_info_path = Path.Combine (TargetFolder, ".git", "info"); + + if (!Directory.Exists (git_info_path)) + Directory.CreateDirectory (git_info_path); + + string exclude_rules = string.Join (Environment.NewLine, ExcludeRules); + string exclude_rules_file_path = Path.Combine (git_info_path, "exclude"); + + File.WriteAllText (exclude_rules_file_path, exclude_rules); + } + + + void InstallAttributeRules () + { + string git_attributes_file_path = Path.Combine (TargetFolder, ".git", "info", "attributes"); + Directory.CreateDirectory (Path.GetDirectoryName (git_attributes_file_path)); + + if (FetchedRepoStorageType == StorageType.LargeFiles) { + File.WriteAllText (git_attributes_file_path, "* filter=lfs diff=lfs merge=lfs -text"); + return; + } + + if (FetchedRepoStorageType == StorageType.Encrypted) { + File.WriteAllText (git_attributes_file_path, "* filter=encryption -diff -delta merge=binary"); + return; + } + + TextWriter writer = new StreamWriter (git_attributes_file_path); + + // Treat all files as binary as we always want to keep both file versions on a conflict + writer.WriteLine ("* merge=binary"); + + // Compile a list of files we don't want Git to compress. Not compressing + // already compressed files decreases memory usage and increases speed + string [] extensions = { + "jpg", "jpeg", "png", "tiff", "gif", // Images + "flac", "mp3", "ogg", "oga", // Audio + "avi", "mov", "mpg", "mpeg", "mkv", "ogv", "ogx", "webm", // Video + "zip", "gz", "bz", "bz2", "rpm", "deb", "tgz", "rar", "ace", "7z", "pak", "tc", "iso", ".dmg" // Archives + }; + + foreach (string extension in extensions) { + writer.WriteLine ("*." + extension + " -delta merge=binary"); + writer.WriteLine ("*." + extension.ToUpper () + " -delta merge=binary"); + } + + writer.Close (); + } + + + void InstallGitLFS () + { + var git_config_required = new SubversionCommand (TargetFolder, "config filter.lfs.required true"); + + string GIT_SSH_COMMAND = SubversionCommand.FormatGitSSHCommand (auth_info); + string smudge_command; + string clean_command; + + if (InstallationInfo.OperatingSystem == OS.Mac) { + smudge_command = "env GIT_SSH_COMMAND='" + GIT_SSH_COMMAND + "' " + + Path.Combine (Configuration.DefaultConfiguration.BinPath, "git-lfs") + " smudge %f"; + + clean_command = Path.Combine (Configuration.DefaultConfiguration.BinPath, "git-lfs") + " clean %f"; + + } else { + smudge_command = "env GIT_SSH_COMMAND='" + GIT_SSH_COMMAND + "' git-lfs smudge %f"; + clean_command = "git-lfs clean %f"; + } + + var git_config_smudge = new SubversionCommand (TargetFolder, + string.Format ("config filter.lfs.smudge \"{0}\"", smudge_command)); + + var git_config_clean = new SubversionCommand (TargetFolder, + string.Format ("config filter.lfs.clean '{0}'", clean_command)); + + git_config_required.StartAndWaitForExit (); + git_config_clean.StartAndWaitForExit (); + git_config_smudge.StartAndWaitForExit (); + } + } +} diff --git a/Sparkles/Subversion/SubversionRepository.cs b/Sparkles/Subversion/SubversionRepository.cs new file mode 100644 index 000000000..e9422419d --- /dev/null +++ b/Sparkles/Subversion/SubversionRepository.cs @@ -0,0 +1,1081 @@ +// SparkleShare, a collaboration and sharing tool. +// Copyright (C) 2010 Hylke Bons +// +// 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 user_is_set; + + + string cached_branch; + + string branch { + get { + if (!string.IsNullOrEmpty (this.cached_branch)) + return this.cached_branch; + + var git = new SubversionCommand (LocalPath, "config core.ignorecase true"); + git.StartAndWaitForExit (); + + // TODO: ugly + while (this.in_merge && HasLocalChanges) { + try { + ResolveConflict (); + + } catch (IOException e) { + Logger.LogInfo ("Git", Name + " | Failed to resolve conflict, trying again...", e); + } + } + + git = new SubversionCommand (LocalPath, "config core.ignorecase false"); + git.StartAndWaitForExit (); + + git = new SubversionCommand (LocalPath, "rev-parse --abbrev-ref HEAD"); + this.cached_branch = git.StartAndReadStandardOutput (); + + return this.cached_branch; + } + } + + + bool in_merge { + get { + string merge_file_path = Path.Combine (LocalPath, ".git", "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; + + var git_config = new SubversionCommand (LocalPath, "config core.ignorecase false"); + git_config.StartAndWaitForExit (); + + git_config = new SubversionCommand (LocalPath, "config remote.origin.url \"" + RemoteUrl + "\""); + git_config.StartAndWaitForExit (); + } + + + public override List ExcludePaths { + get { + List rules = new List (); + rules.Add (".git"); + + return rules; + } + } + + + public override double Size { + get { + string file_path = Path.Combine (LocalPath, ".git", "info", "size"); + + try { + string size = File.ReadAllText (file_path); + return double.Parse (size); + + } catch { + return 0; + } + } + } + + + public override double HistorySize { + get { + string file_path = Path.Combine (LocalPath, ".git", "info", "history_size"); + + try { + string size = File.ReadAllText (file_path); + return double.Parse (size); + + } catch { + return 0; + } + } + } + + + void UpdateSizes () + { + double size = CalculateSizes (new DirectoryInfo (LocalPath)); + double history_size = CalculateSizes (new DirectoryInfo (Path.Combine (LocalPath, ".git"))); + + string size_file_path = Path.Combine (LocalPath, ".git", "info", "size"); + string history_size_file_path = Path.Combine (LocalPath, ".git", "info", "history_size"); + + File.WriteAllText (size_file_path, size.ToString ()); + File.WriteAllText (history_size_file_path, history_size.ToString ()); + } + + + public override string CurrentRevision { + get { + var git = new SubversionCommand (LocalPath, "rev-parse HEAD"); + string output = git.StartAndReadStandardOutput (); + + if (git.ExitCode == 0) + return output; + + return null; + } + } + + + public override bool HasRemoteChanges { + get { + Logger.LogInfo ("Git", Name + " | Checking for remote changes..."); + string current_revision = CurrentRevision; + + var git = new SubversionCommand (LocalPath, + "ls-remote --heads --exit-code origin " + this.branch, auth_info); + + string output = git.StartAndReadStandardOutput (); + + if (git.ExitCode != 0) + return false; + + string remote_revision = "" + output.Substring (0, 40); + + if (!remote_revision.Equals (current_revision)) { + git = new SubversionCommand (LocalPath, "merge-base " + remote_revision + " master"); + git.StartAndWaitForExit (); + + if (git.ExitCode != 0) { + Logger.LogInfo ("Git", Name + " | Remote changes found, local: " + + current_revision + ", remote: " + remote_revision); + + Error = ErrorStatus.None; + return true; + + } else { + Logger.LogInfo ("Git", Name + " | Remote " + remote_revision + " is already in our history"); + return false; + } + } + + Logger.LogInfo ("Git", 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 (); + + if (message != null) + Commit (message); + + string pre_push_hook_path = Path.Combine (LocalPath, ".git", "hooks", "pre-push"); + string pre_push_hook_content; + + // The pre-push hook may have been changed by Git LFS, overwrite it to use our own configuration + if (InstallationInfo.OperatingSystem == OS.Mac) { + pre_push_hook_content = + "#!/bin/sh" + Environment.NewLine + + "env GIT_SSH_COMMAND='" + SubversionCommand.FormatGitSSHCommand (auth_info) + "' " + + Path.Combine (Configuration.DefaultConfiguration.BinPath, "git-lfs") + " pre-push \"$@\""; + + } else { + pre_push_hook_content = + "#!/bin/sh" + Environment.NewLine + + "env GIT_SSH_COMMAND='" + SubversionCommand.FormatGitSSHCommand (auth_info) + "' " + + "git-lfs pre-push \"$@\""; + } + + Directory.CreateDirectory (Path.GetDirectoryName (pre_push_hook_path)); + File.WriteAllText (pre_push_hook_path, pre_push_hook_content); + + var git_push = new SubversionCommand (LocalPath, string.Format ("push --all --progress origin", RemoteUrl), auth_info); + git_push.StartInfo.RedirectStandardError = true; + git_push.Start (); + + if (!ReadStream (git_push)) + return false; + + git_push.WaitForExit (); + + UpdateSizes (); + + if (git_push.ExitCode == 0) + return true; + + Error = ErrorStatus.HostUnreachable; + return false; + } + + + public override bool SyncDown () + { + string lfs_is_behind_file_path = Path.Combine (LocalPath, ".git", "lfs", "is_behind"); + + if (StorageType == StorageType.LargeFiles) + File.Create (lfs_is_behind_file_path); + + var git_fetch = new SubversionCommand (LocalPath, "fetch --progress origin " + branch, auth_info); + + git_fetch.StartInfo.RedirectStandardError = true; + git_fetch.Start (); + + if (!ReadStream (git_fetch)) + return false; + + git_fetch.WaitForExit (); + + if (git_fetch.ExitCode != 0) { + Error = ErrorStatus.HostUnreachable; + return false; + } + + if (Merge ()) { + if (StorageType == StorageType.LargeFiles) { + // Pull LFS files manually to benefit from concurrency + var git_lfs_pull = new SubversionCommand (LocalPath, "lfs pull origin", auth_info); + git_lfs_pull.StartAndWaitForExit (); + + if (git_lfs_pull.ExitCode != 0) { + Error = ErrorStatus.HostUnreachable; + return false; + } + + if (File.Exists (lfs_is_behind_file_path)) + File.Delete (lfs_is_behind_file_path); + } + + UpdateSizes (); + return true; + } + + return false; + } + + + bool ReadStream (SubversionCommand command) + { + StreamReader output_stream = command.StandardError; + + if (StorageType == StorageType.LargeFiles) + output_stream = command.StandardOutput; + + double percentage = 0; + double speed = 0; + string information = ""; + + while (!output_stream.EndOfStream) { + string line = output_stream.ReadLine (); + ErrorStatus error = SubversionCommand.ParseProgress (line, out percentage, out speed, out information); + + if (error != ErrorStatus.None) { + Error = error; + information = line; + + command.Kill (); + command.Dispose (); + Logger.LogInfo ("Git", Name + " | Error status changed to " + Error); + + return false; + } + + OnProgressChanged (percentage, speed, information); + } + + return true; + } + + + public override bool HasLocalChanges { + get { + PrepareDirectories (LocalPath); + + var git = new SubversionCommand (LocalPath, "status --porcelain"); + string output = git.StartAndReadStandardOutput (); + + return !string.IsNullOrEmpty (output); + } + } + + + public override bool HasUnsyncedChanges { + get { + if (StorageType == StorageType.LargeFiles) { + string lfs_is_behind_file_path = Path.Combine (LocalPath, ".git", "lfs", "is_behind"); + + if (File.Exists (lfs_is_behind_file_path)) + return true; + } + + string unsynced_file_path = Path.Combine (LocalPath, ".git", "has_unsynced_changes"); + return File.Exists (unsynced_file_path); + } + + set { + string unsynced_file_path = Path.Combine (LocalPath, ".git", "has_unsynced_changes"); + + if (value) + File.WriteAllText (unsynced_file_path, ""); + else + File.Delete (unsynced_file_path); + } + } + + + // Stages the made changes + bool Add () + { + var git = new SubversionCommand (LocalPath, "add --all"); + git.StartAndWaitForExit (); + + return (git.ExitCode == 0); + } + + + // Commits the made changes + void Commit (string message) + { + SubversionCommand git; + + if (!this.user_is_set) { + git = new SubversionCommand (LocalPath, "config user.name \"" + base.local_config.User.Name + "\""); + git.StartAndWaitForExit (); + + git = new SubversionCommand (LocalPath, "config user.email \"" + base.local_config.User.Email + "\""); + git.StartAndWaitForExit (); + + this.user_is_set = true; + } + + git = new SubversionCommand (LocalPath, "commit --all --message=\"" + message + "\" " + + "--author=\"" + base.local_config.User.Name + " <" + base.local_config.User.Email + ">\""); + + git.StartAndReadStandardOutput (); + } + + + // Merges the fetched changes + bool Merge () + { + string message = FormatCommitMessage (); + + if (message != null) { + Add (); + Commit (message); + } + + SubversionCommand git; + + // Stop if we're already in a merge because something went wrong + if (this.in_merge) { + git = new SubversionCommand (LocalPath, "merge --abort"); + git.StartAndWaitForExit (); + + return false; + } + + // Temporarily change the ignorecase setting to true to avoid + // conflicts in file names due to letter case changes + git = new SubversionCommand (LocalPath, "config core.ignorecase true"); + git.StartAndWaitForExit (); + + git = new SubversionCommand (LocalPath, "merge FETCH_HEAD"); + git.StartInfo.RedirectStandardOutput = false; + + string error_output = git.StartAndReadStandardError (); + + if (git.ExitCode != 0) { + // Stop when we can't merge due to locked local files + // error: cannot stat 'filename': Permission denied + if (error_output.Contains ("error: cannot stat")) { + Error = ErrorStatus.UnreadableFiles; + Logger.LogInfo ("Git", Name + " | Error status changed to " + Error); + + git = new SubversionCommand (LocalPath, "merge --abort"); + git.StartAndWaitForExit (); + + git = new SubversionCommand (LocalPath, "config core.ignorecase false"); + git.StartAndWaitForExit (); + + return false; + + } else { + Logger.LogInfo ("Git", error_output); + Logger.LogInfo ("Git", Name + " | Conflict detected, trying to get out..."); + + while (this.in_merge && HasLocalChanges) { + try { + ResolveConflict (); + + } catch (Exception e) { + Logger.LogInfo ("Git", Name + " | Failed to resolve conflict, trying again...", e); + } + } + + Logger.LogInfo ("Git", Name + " | Conflict resolved"); + } + } + + git = new SubversionCommand (LocalPath, "config core.ignorecase false"); + git.StartAndWaitForExit (); + + return true; + } + + + void ResolveConflict () + { + // This is a 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 server's, save ours as a timestamped copy + // UD unmerged, deleted by them -> Use ours + // UA unmerged, added by them -> Use server's, save ours as a timestamped copy + // DU unmerged, deleted by us -> Use server's + // AA unmerged, both added -> Use server's, save ours as a timestamped copy + // UU unmerged, both modified -> Use server's, save ours as a timestamped copy + // ?? unmerged, new files -> Stage the new files + + var git_status = new SubversionCommand (LocalPath, "status --porcelain"); + string output = git_status.StartAndReadStandardOutput (); + + string [] lines = output.Split ("\n".ToCharArray ()); + bool trigger_conflict_event = false; + + foreach (string line in lines) { + string conflicting_path = line.Substring (3); + conflicting_path = EnsureSpecialCharacters (conflicting_path); + conflicting_path = conflicting_path.Trim ("\"".ToCharArray ()); + + // Remove possible rename indicators + string [] separators = {" -> \"", " -> "}; + foreach (string separator in separators) { + if (conflicting_path.Contains (separator)) { + conflicting_path = conflicting_path.Substring ( + conflicting_path.IndexOf (separator) + separator.Length); + } + } + + Logger.LogInfo ("Git", Name + " | Conflict type: " + line); + + // Ignore conflicts in hidden files and use the local versions + if (conflicting_path.EndsWith (".sparkleshare") || conflicting_path.EndsWith (".empty")) { + Logger.LogInfo ("Git", Name + " | Ignoring conflict in special file: " + conflicting_path); + + // Recover local version + var git_ours = new SubversionCommand (LocalPath, "checkout --ours \"" + conflicting_path + "\""); + git_ours.StartAndWaitForExit (); + + string abs_conflicting_path = Path.Combine (LocalPath, conflicting_path); + + if (File.Exists (abs_conflicting_path)) + File.SetAttributes (abs_conflicting_path, FileAttributes.Hidden); + + continue; + } + + Logger.LogInfo ("Git", Name + " | Resolving: " + conflicting_path); + + // 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 + var git_ours = new SubversionCommand (LocalPath, "checkout --ours \"" + conflicting_path + "\""); + git_ours.StartAndWaitForExit (); + + // Append a timestamp to local version. + // Windows doesn't allow colons in the file name, so + // we use "h" between the hours and minutes instead. + string timestamp = DateTime.Now.ToString ("MMM d H\\hmm"); + string our_path = Path.GetFileNameWithoutExtension (conflicting_path) + + " (" + base.local_config.User.Name + ", " + timestamp + ")" + Path.GetExtension (conflicting_path); + + string abs_conflicting_path = Path.Combine (LocalPath, conflicting_path); + string abs_our_path = Path.Combine (LocalPath, our_path); + + if (File.Exists (abs_conflicting_path) && !File.Exists (abs_our_path)) + File.Move (abs_conflicting_path, abs_our_path); + + // Recover server version + var git_theirs = new SubversionCommand (LocalPath, "checkout --theirs \"" + conflicting_path + "\""); + git_theirs.StartAndWaitForExit (); + + trigger_conflict_event = true; + + + // The server version has been modified, but the local version was removed + } else 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 + var git_add = new SubversionCommand (LocalPath, "add \"" + conflicting_path + "\""); + git_add.StartAndWaitForExit (); + + + // The local version has been modified, but the server version was removed + } else if (line.StartsWith ("UD")) { + + // Recover server version + var git_theirs = new SubversionCommand (LocalPath, "checkout --theirs \"" + conflicting_path + "\""); + git_theirs.StartAndWaitForExit (); + + + // Server and local versions were removed + } else if (line.StartsWith ("DD")) { + Logger.LogInfo ("Git", Name + " | No need to resolve: " + line); + + // New local files + } else if (line.StartsWith ("??")) { + Logger.LogInfo ("Git", Name + " | Found new file, no need to resolve: " + line); + + } else { + Logger.LogInfo ("Git", Name + " | Don't know what to do with: " + line); + } + } + + Add (); + + var git = new SubversionCommand (LocalPath, "commit --message \"Conflict resolution by SparkleShare\""); + git.StartInfo.RedirectStandardOutput = false; + git.StartAndWaitForExit (); + + if (trigger_conflict_event) + OnConflictResolved (); + } + + + 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 ("Git", Name + " | Restoring \"" + path + "\" (revision " + revision + ")"); + + // Restore the older file... + var git = new SubversionCommand (LocalPath, "checkout " + revision + " \"" + path + "\""); + git.StartAndWaitForExit (); + + string local_file_path = Path.Combine (LocalPath, path); + + // ...move it... + try { + File.Move (local_file_path, target_file_path); + + } catch { + Logger.LogInfo ("Git", + Name + " | Could not move \"" + local_file_path + "\" to \"" + target_file_path + "\""); + } + + // ...and restore the most recent revision + git = new SubversionCommand (LocalPath, "checkout " + CurrentRevision + " \"" + path + "\""); + git.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 git; + + if (path == null) { + git = new SubversionCommand (LocalPath, "--no-pager log --since=1.month --raw --find-renames --date=iso " + + "--format=medium --no-color --no-merges"); + + } else { + path = path.Replace ("\\", "/"); + + git = new SubversionCommand (LocalPath, "--no-pager log --raw --find-renames --date=iso " + + "--format=medium --no-color --no-merges -- \"" + path + "\""); + } + + string output = git.StartAndReadStandardOutput (); + + if (path == null && string.IsNullOrWhiteSpace (output)) { + git = new SubversionCommand (LocalPath, "--no-pager log -n 75 --raw --find-renames --date=iso " + + "--format=medium --no-color --no-merges"); + + output = git.StartAndReadStandardOutput (); + } + + string [] lines = output.Split ("\n".ToCharArray ()); + List entries = new List (); + + // Split up commit entries + int line_number = 0; + bool first_pass = true; + string entry = "", last_entry = ""; + foreach (string line in lines) { + if (line.StartsWith ("commit") && !first_pass) { + entries.Add (entry); + entry = ""; + line_number = 0; + + } else { + first_pass = false; + } + + // Only parse first 250 files to prevent memory issues + if (line_number < 250) { + entry += line + "\n"; + line_number++; + } + + last_entry = entry; + } + + entries.Add (last_entry); + + // 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 (".git")) { + if (child_path.Equals (Path.Combine (LocalPath, ".git"))) + 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 ("Git", 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 ("Git", Name + " | Failed adding empty folder " + path); + } + } + } + + } catch (IOException e) { + Logger.LogInfo ("Git", "Failed preparing directory", e); + } + } + + + + List ParseStatus () + { + List changes = new List (); + + var git_status = new SubversionCommand (LocalPath, "status --porcelain"); + git_status.Start (); + + while (!git_status.StandardOutput.EndOfStream) { + string line = git_status.StandardOutput.ReadLine (); + line = line.Trim (); + + if (line.EndsWith (".empty") || line.EndsWith (".empty\"")) + line = line.Replace (".empty", ""); + + 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); + } + + git_status.StandardOutput.ReadToEnd (); + git_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 (".git") || + directory.Name.Equals ("rebase-apply")) { + + 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); + } +} From 11da3f129820dfbb900c96c38f060a73afe23618 Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Tue, 23 Aug 2016 08:14:25 -0400 Subject: [PATCH 3/6] more of Subversion work --- SparkleShare/Common/BaseController.cs | 1 + SparkleShare/Linux/SparkleShare.Linux.csproj | 4 + SparkleShare/Mac/SparkleShare.Mac.csproj | 6 +- Sparkles/BaseFetcher.cs | 4 + Sparkles/Command.cs | 2 +- Sparkles/Subversion/SubversionCommand.cs | 103 +--- Sparkles/Subversion/SubversionFetcher.cs | 317 ++--------- Sparkles/Subversion/SubversionRepository.cs | 520 +++---------------- 8 files changed, 143 insertions(+), 814 deletions(-) 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/Linux/SparkleShare.Linux.csproj b/SparkleShare/Linux/SparkleShare.Linux.csproj index d988170c9..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 diff --git a/SparkleShare/Mac/SparkleShare.Mac.csproj b/SparkleShare/Mac/SparkleShare.Mac.csproj index 3db3e27be..4cd987aa0 100644 --- a/SparkleShare/Mac/SparkleShare.Mac.csproj +++ b/SparkleShare/Mac/SparkleShare.Mac.csproj @@ -253,7 +253,7 @@ Presets\subversion%402x.png - + Resources\text-balloon.png @@ -288,5 +288,9 @@ {009FDCD7-1D57-4202-BB6D-8477D8C6B8EE} Sparkles.Git + + {009FDCD7-1D57-4202-BB6D-8477D8C6AAAA} + Sparkles.Subversion + 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/SubversionCommand.cs b/Sparkles/Subversion/SubversionCommand.cs index 809365176..19d858277 100644 --- a/Sparkles/Subversion/SubversionCommand.cs +++ b/Sparkles/Subversion/SubversionCommand.cs @@ -1,5 +1,6 @@ // 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 @@ -21,84 +22,57 @@ namespace Sparkles.Subversion { public class SubversionCommand : Command { - public static string SSHPath = "ssh"; public static string ExecPath; - static string git_path; + static string svn_path; - public static string GitPath { + public static string SvnPath { get { - if (git_path == null) - git_path = LocateCommand ("svn"); + if (svn_path == null) + svn_path = LocateCommand ("svn"); - return git_path; + return svn_path; } set { - git_path = value; + svn_path = value; } } - public static string GitVersion { + public static string SvnVersion { get { - if (GitPath == null) - GitPath = LocateCommand ("svn"); + if (SvnPath == null) + SvnPath = LocateCommand ("svn"); - var git_version = new Command (GitPath, "--version", false); + var svn_version = new Command (SvnPath, "--version", false); if (ExecPath != null) - git_version.SetEnvironmentVariable ("GIT_EXEC_PATH", ExecPath); + svn_version.SetEnvironmentVariable ("SVN_EXEC_PATH", ExecPath); - string version = git_version.StartAndReadStandardOutput (); - return version.Replace ("svn version ", ""); + string version = svn_version.StartAndReadStandardOutput (); + return version.Substring(0, version.IndexOf("\n")).Replace ("svn, version ", ""); } } - - public static string GitLFSVersion { - get { - if (GitPath == null) - GitPath = LocateCommand ("git"); - - var git_lfs_version = new Command (GitPath, "lfs version", false); - - if (ExecPath != null) - git_lfs_version.SetEnvironmentVariable ("GIT_EXEC_PATH", ExecPath); - - string version = git_lfs_version.StartAndReadStandardOutput (); - return version.Replace ("git-lfs/", "").Split (' ') [0]; - } - } - - public SubversionCommand (string working_dir, string args) : this (working_dir, args, null) { } - public SubversionCommand (string working_dir, string args, SSHAuthenticationInfo auth_info) : base (GitPath, args) + public SubversionCommand (string working_dir, string args, SSHAuthenticationInfo auth_info) : base (SvnPath, args) { StartInfo.WorkingDirectory = working_dir; - string GIT_SSH_COMMAND = SSHPath; - - if (auth_info != null) - GIT_SSH_COMMAND = FormatGitSSHCommand (auth_info); - if (ExecPath != null) - SetEnvironmentVariable ("GIT_EXEC_PATH", ExecPath); + SetEnvironmentVariable ("SVN_EXEC_PATH", ExecPath); - SetEnvironmentVariable ("GIT_SSH_COMMAND", GIT_SSH_COMMAND); - SetEnvironmentVariable ("GIT_TERMINAL_PROMPT", "0"); SetEnvironmentVariable ("LANG", "en_US"); } static Regex progress_regex = new Regex (@"([0-9]+)%", RegexOptions.Compiled); - static Regex progress_regex_lfs = new Regex (@".*\(([0-9]+) of ([0-9]+) files\).*", RegexOptions.Compiled); - static Regex progress_regex_lfs_skipped = new Regex (@".*\(([0-9]+) of ([0-9]+) files, ([0-9]+) skipped\).*", RegexOptions.Compiled); static Regex speed_regex = new Regex (@"([0-9\.]+) ([KM])iB/s", RegexOptions.Compiled); public static ErrorStatus ParseProgress (string line, out double percentage, out double speed, out string information) @@ -109,43 +83,11 @@ public static ErrorStatus ParseProgress (string line, out double percentage, out Match match; - if (line.StartsWith ("Git LFS:")) { - match = progress_regex_lfs_skipped.Match (line); - - int current_file = 0; - int total_file_count = 0; - int skipped_file_count = 0; - - if (match.Success) { - // "skipped" files are objects that have already been transferred - skipped_file_count = int.Parse (match.Groups [3].Value); - - } else { - - match = progress_regex_lfs.Match (line); - - if (!match.Success) - return ErrorStatus.None; - } - - current_file = int.Parse (match.Groups [1].Value); - - if (current_file == 0) - return ErrorStatus.None; - - total_file_count = int.Parse (match.Groups [2].Value) - skipped_file_count; - - percentage = Math.Round ((double) current_file / total_file_count * 100, 0); - information = string.Format ("{0} of {1} files", current_file, total_file_count); - - return ErrorStatus.None; - } - match = progress_regex.Match (line); if (!match.Success || string.IsNullOrWhiteSpace (line)) { if (!string.IsNullOrWhiteSpace (line)) - Logger.LogInfo ("Git", line); + Logger.LogInfo ("Svn", line); return FindError (line); } @@ -209,16 +151,5 @@ static ErrorStatus FindError (string line) return error; } - - - public static string FormatGitSSHCommand (SSHAuthenticationInfo auth_info) - { - return SSHPath + " " + - "-i " + auth_info.PrivateKeyFilePath.Replace (" ", "\\ ") + " " + - "-o UserKnownHostsFile=" + auth_info.KnownHostsFilePath.Replace (" ", "\\ ") + " " + - "-o IdentitiesOnly=yes" + " " + // Don't fall back to other keys on the system - "-o PasswordAuthentication=no" + " " + // Don't hang on possible password prompts - "-F /dev/null"; // Ignore the system's SSH config file - } } } diff --git a/Sparkles/Subversion/SubversionFetcher.cs b/Sparkles/Subversion/SubversionFetcher.cs index 8343d09e5..9ddf35cc7 100644 --- a/Sparkles/Subversion/SubversionFetcher.cs +++ b/Sparkles/Subversion/SubversionFetcher.cs @@ -1,5 +1,6 @@ // 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 @@ -23,48 +24,28 @@ namespace Sparkles.Subversion { - public class GitFetcher : SSHFetcher { + public class SubversionFetcher : SSHFetcher { - SubversionCommand git_clone; + SubversionCommand svn_checkout; SSHAuthenticationInfo auth_info; - string password_salt = Path.GetRandomFileName ().SHA256 ().Substring (0, 16); - - protected override bool IsFetchedRepoEmpty { get { - var git_rev_parse = new SubversionCommand (TargetFolder, "rev-parse HEAD"); - git_rev_parse.StartAndWaitForExit (); + var svn_rev_parse = new SubversionCommand (TargetFolder, "ls -v -r"); + svn_rev_parse.StartAndWaitForExit (); - return (git_rev_parse.ExitCode != 0); + return (svn_rev_parse.ExitCode != 0); } } - public GitFetcher (SparkleFetcherInfo fetcher_info, SSHAuthenticationInfo auth_info) : base (fetcher_info) + public SubversionFetcher (SparkleFetcherInfo fetcher_info, SSHAuthenticationInfo auth_info) : base (fetcher_info) { + this.auth_info = auth_info; var uri_builder = new UriBuilder (RemoteUrl); - if (!RemoteUrl.Scheme.Equals ("ssh") && !RemoteUrl.Scheme.Equals ("git")) - uri_builder.Scheme = "ssh"; - - if (RemoteUrl.Host.Equals ("github.com") || - RemoteUrl.Host.Equals ("gitlab.com")) { - - AvailableStorageTypes.Add ( - new StorageTypeInfo (StorageType.LargeFiles, "Large File Storage", - "Trade off versioning to save space;\nkeeps file history on the host only")); - - uri_builder.Scheme = "ssh"; - uri_builder.UserName = "git"; - - if (!RemoteUrl.AbsolutePath.EndsWith (".git")) - uri_builder.Path += ".git"; - - } else if (string.IsNullOrEmpty (RemoteUrl.UserInfo)) { - uri_builder.UserName = "storage"; - } + uri_builder.Scheme = "svn"; RemoteUrl = uri_builder.Uri; @@ -79,32 +60,27 @@ public override bool Fetch () if (!base.Fetch ()) return false; - StorageType? storage_type = DetermineStorageType (); + StorageType? storage_type = StorageType.Plain; if (storage_type == null) return false; FetchedRepoStorageType = (StorageType) storage_type; - string git_clone_command = "clone --progress --no-checkout"; + string svn_checkout_command = "checkout"; - if (!FetchPriorHistory) - git_clone_command += " --depth=1"; - if (storage_type == StorageType.LargeFiles) - git_clone_command = "lfs clone --progress --no-checkout"; - - git_clone = new SubversionCommand (Configuration.DefaultConfiguration.TmpPath, - string.Format ("{0} \"{1}\" \"{2}\"", git_clone_command, RemoteUrl, TargetFolder), + svn_checkout = new SubversionCommand (Configuration.DefaultConfiguration.TmpPath, + string.Format ("{0} \"{1}\" \"{2}\"", svn_checkout_command, RemoteUrl, TargetFolder), auth_info); - git_clone.StartInfo.RedirectStandardError = true; - git_clone.Start (); + svn_checkout.StartInfo.RedirectStandardError = true; + svn_checkout.Start (); - StreamReader output_stream = git_clone.StandardError; + StreamReader output_stream = svn_checkout.StandardError; if (FetchedRepoStorageType == StorageType.LargeFiles) - output_stream = git_clone.StandardOutput; + output_stream = svn_checkout.StandardOutput; double percentage = 0; double speed = 0; @@ -117,8 +93,8 @@ public override bool Fetch () if (error != ErrorStatus.None) { IsActive = false; - git_clone.Kill (); - git_clone.Dispose (); + svn_checkout.Kill (); + svn_checkout.Dispose (); return false; } @@ -126,9 +102,9 @@ public override bool Fetch () OnProgressChanged (percentage, speed, information); } - git_clone.WaitForExit (); + svn_checkout.WaitForExit (); - if (git_clone.ExitCode != 0) + if (svn_checkout.ExitCode != 0) return false; Thread.Sleep (500); @@ -142,9 +118,9 @@ public override bool Fetch () public override void Stop () { try { - if (git_clone != null && !git_clone.HasExited) { - git_clone.Kill (); - git_clone.Dispose (); + if (svn_checkout != null && !svn_checkout.HasExited) { + svn_checkout.Kill (); + svn_checkout.Dispose (); } } catch (Exception e) { @@ -168,60 +144,15 @@ public override string Complete (StorageType selected_storage_type) string identifier = base.Complete (selected_storage_type); string identifier_path = Path.Combine (TargetFolder, ".sparkleshare"); - InstallConfiguration (); - InstallGitLFS (); - - InstallAttributeRules (); - InstallExcludeRules (); - - if (IsFetchedRepoEmpty) { - File.WriteAllText (identifier_path, identifier); + File.WriteAllText (identifier_path, identifier); - var git_add = new SubversionCommand (TargetFolder, "add .sparkleshare"); - var git_commit = new SubversionCommand (TargetFolder, "commit --message=\"Initial commit by SparkleShare\""); + 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 - git_add.StartAndWaitForExit (); - git_commit.StartAndWaitForExit (); + // We can't do the "commit --all" shortcut because it doesn't add untracked files + svn_add.StartAndWaitForExit (); + svn_commit.StartAndWaitForExit (); - // These branches will be pushed later by "git push --all" - if (selected_storage_type == StorageType.LargeFiles) { - var git_branch = new SubversionCommand (TargetFolder, "branch x-sparkleshare-lfs", auth_info); - git_branch.StartAndWaitForExit (); - } - - if (selected_storage_type == StorageType.Encrypted) { - var git_branch = new SubversionCommand (TargetFolder, - string.Format ("branch x-sparkleshare-encrypted-{0}", password_salt), auth_info); - - git_branch.StartAndWaitForExit (); - } - - } else { - if (File.Exists (identifier_path)) - identifier = File.ReadAllText (identifier_path).Trim (); - - string branch = "HEAD"; - string prefered_branch = "SparkleShare"; - - // Prefer the "SparkleShare" branch if it exists - var git_show_ref = new SubversionCommand (TargetFolder, - "show-ref --verify --quiet refs/heads/" + prefered_branch); - - git_show_ref.StartAndWaitForExit (); - - if (git_show_ref.ExitCode == 0) - branch = prefered_branch; - - var git_checkout = new SubversionCommand (TargetFolder, string.Format ("checkout --quiet --force {0}", branch)); - git_checkout.StartAndWaitForExit (); - } - - // git-lfs may leave junk behind - string git_lfs_tmp_path = Path.Combine (Configuration.DefaultConfiguration.TmpPath, "lfs"); - - if (Directory.Exists (git_lfs_tmp_path)) - Directory.Delete (git_lfs_tmp_path, true); File.SetAttributes (identifier_path, FileAttributes.Hidden); return identifier; @@ -230,53 +161,12 @@ public override string Complete (StorageType selected_storage_type) public override void EnableFetchedRepoCrypto (string password) { - string password_file = ".git/info/encryption_password"; - var git_config_required = new SubversionCommand (TargetFolder, "config filter.encryption.required true"); - - var git_config_smudge = new SubversionCommand (TargetFolder, "config filter.encryption.smudge " + - string.Format ("\"openssl enc -d -aes-256-cbc -base64 -S {0} -pass file:{1}\"", password_salt, password_file)); - - var git_config_clean = new SubversionCommand (TargetFolder, "config filter.encryption.clean " + - string.Format ("\"openssl enc -e -aes-256-cbc -base64 -S {0} -pass file:{1}\"", password_salt, password_file)); - - git_config_required.StartAndWaitForExit (); - git_config_smudge.StartAndWaitForExit (); - git_config_clean.StartAndWaitForExit (); - - // Store the password, TODO: 600 permissions - string password_file_path = Path.Combine (TargetFolder, ".git", "info", "encryption_password"); - File.WriteAllText (password_file_path, password.SHA256 (password_salt)); } public override bool IsFetchedRepoPasswordCorrect (string password) { - string password_check_file_path = Path.Combine (TargetFolder, ".sparkleshare"); - - if (!File.Exists (password_check_file_path)) { - var git_show = new SubversionCommand (TargetFolder, "show HEAD:.sparkleshare"); - string output = git_show.StartAndReadStandardOutput (); - - if (git_show.ExitCode == 0) - File.WriteAllText (password_check_file_path, output); - else - return false; - } - - string args = string.Format ("enc -d -aes-256-cbc -base64 -S {0} -pass pass:{1} -in \"{2}\"", - password_salt, password.SHA256 (password_salt), password_check_file_path); - - var process = new Command ("openssl", args); - - process.StartInfo.WorkingDirectory = TargetFolder; - process.StartAndWaitForExit (); - - if (process.ExitCode == 0) { - File.Delete (password_check_file_path); - return true; - } - - return false; + return true; } @@ -285,153 +175,12 @@ public override string FormatName () string name = Path.GetFileName (RemoteUrl.AbsolutePath); name = name.ReplaceUnderscoreWithSpace (); - if (name.EndsWith (".git")) - name = name.Replace (".git", ""); + if (name.EndsWith (".svn")) + name = name.Replace (".svn", ""); return name; } - StorageType? DetermineStorageType () - { - var git_ls_remote = new SubversionCommand (Configuration.DefaultConfiguration.TmpPath, - string.Format ("ls-remote --heads \"{0}\"", RemoteUrl), auth_info); - - string output = git_ls_remote.StartAndReadStandardOutput (); - - if (git_ls_remote.ExitCode != 0) - return null; - - if (string.IsNullOrWhiteSpace (output)) - return StorageType.Unknown; - - foreach (string line in output.Split ("\n".ToCharArray ())) { - string [] line_parts = line.Split ('/'); - string branch = line_parts [line_parts.Length - 1]; - - if (branch == "x-sparkleshare-lfs") - return StorageType.LargeFiles; - - string encrypted_storage_prefix = "x-sparkleshare-encrypted-"; - - if (branch.StartsWith (encrypted_storage_prefix)) { - password_salt = branch.Replace (encrypted_storage_prefix, ""); - return StorageType.Encrypted; - } - } - - return StorageType.Plain; - } - - - void InstallConfiguration () - { - string [] settings = { - "core.autocrlf input", - "core.quotepath false", // Don't quote "unusual" characters in path names - "core.ignorecase false", // Be case sensitive explicitly to work on Mac - "core.filemode false", // Ignore permission changes - "core.precomposeunicode true", // Use the same Unicode form on all filesystems - "core.safecrlf false", - "core.excludesfile \"\"", - "core.packedGitLimit 128m", // Some memory limiting options - "core.packedGitWindowSize 128m", - "pack.deltaCacheSize 128m", - "pack.packSizeLimit 128m", - "pack.windowMemory 128m", - "push.default matching" - }; - - if (InstallationInfo.OperatingSystem == OS.Windows) - settings [0] = "core.autocrlf true"; - - foreach (string setting in settings) { - var git_config = new SubversionCommand (TargetFolder, "config " + setting); - git_config.StartAndWaitForExit (); - } - } - - - void InstallExcludeRules () - { - string git_info_path = Path.Combine (TargetFolder, ".git", "info"); - - if (!Directory.Exists (git_info_path)) - Directory.CreateDirectory (git_info_path); - - string exclude_rules = string.Join (Environment.NewLine, ExcludeRules); - string exclude_rules_file_path = Path.Combine (git_info_path, "exclude"); - - File.WriteAllText (exclude_rules_file_path, exclude_rules); - } - - - void InstallAttributeRules () - { - string git_attributes_file_path = Path.Combine (TargetFolder, ".git", "info", "attributes"); - Directory.CreateDirectory (Path.GetDirectoryName (git_attributes_file_path)); - - if (FetchedRepoStorageType == StorageType.LargeFiles) { - File.WriteAllText (git_attributes_file_path, "* filter=lfs diff=lfs merge=lfs -text"); - return; - } - - if (FetchedRepoStorageType == StorageType.Encrypted) { - File.WriteAllText (git_attributes_file_path, "* filter=encryption -diff -delta merge=binary"); - return; - } - - TextWriter writer = new StreamWriter (git_attributes_file_path); - - // Treat all files as binary as we always want to keep both file versions on a conflict - writer.WriteLine ("* merge=binary"); - - // Compile a list of files we don't want Git to compress. Not compressing - // already compressed files decreases memory usage and increases speed - string [] extensions = { - "jpg", "jpeg", "png", "tiff", "gif", // Images - "flac", "mp3", "ogg", "oga", // Audio - "avi", "mov", "mpg", "mpeg", "mkv", "ogv", "ogx", "webm", // Video - "zip", "gz", "bz", "bz2", "rpm", "deb", "tgz", "rar", "ace", "7z", "pak", "tc", "iso", ".dmg" // Archives - }; - - foreach (string extension in extensions) { - writer.WriteLine ("*." + extension + " -delta merge=binary"); - writer.WriteLine ("*." + extension.ToUpper () + " -delta merge=binary"); - } - - writer.Close (); - } - - - void InstallGitLFS () - { - var git_config_required = new SubversionCommand (TargetFolder, "config filter.lfs.required true"); - - string GIT_SSH_COMMAND = SubversionCommand.FormatGitSSHCommand (auth_info); - string smudge_command; - string clean_command; - - if (InstallationInfo.OperatingSystem == OS.Mac) { - smudge_command = "env GIT_SSH_COMMAND='" + GIT_SSH_COMMAND + "' " + - Path.Combine (Configuration.DefaultConfiguration.BinPath, "git-lfs") + " smudge %f"; - - clean_command = Path.Combine (Configuration.DefaultConfiguration.BinPath, "git-lfs") + " clean %f"; - - } else { - smudge_command = "env GIT_SSH_COMMAND='" + GIT_SSH_COMMAND + "' git-lfs smudge %f"; - clean_command = "git-lfs clean %f"; - } - - var git_config_smudge = new SubversionCommand (TargetFolder, - string.Format ("config filter.lfs.smudge \"{0}\"", smudge_command)); - - var git_config_clean = new SubversionCommand (TargetFolder, - string.Format ("config filter.lfs.clean '{0}'", clean_command)); - - git_config_required.StartAndWaitForExit (); - git_config_clean.StartAndWaitForExit (); - git_config_smudge.StartAndWaitForExit (); - } } } diff --git a/Sparkles/Subversion/SubversionRepository.cs b/Sparkles/Subversion/SubversionRepository.cs index e9422419d..4522d11ac 100644 --- a/Sparkles/Subversion/SubversionRepository.cs +++ b/Sparkles/Subversion/SubversionRepository.cs @@ -1,5 +1,7 @@ // 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 @@ -27,43 +29,10 @@ namespace Sparkles.Subversion { public class SubversionRepository : BaseRepository { SSHAuthenticationInfo auth_info; - bool user_is_set; - - - string cached_branch; - - string branch { - get { - if (!string.IsNullOrEmpty (this.cached_branch)) - return this.cached_branch; - - var git = new SubversionCommand (LocalPath, "config core.ignorecase true"); - git.StartAndWaitForExit (); - - // TODO: ugly - while (this.in_merge && HasLocalChanges) { - try { - ResolveConflict (); - - } catch (IOException e) { - Logger.LogInfo ("Git", Name + " | Failed to resolve conflict, trying again...", e); - } - } - - git = new SubversionCommand (LocalPath, "config core.ignorecase false"); - git.StartAndWaitForExit (); - - git = new SubversionCommand (LocalPath, "rev-parse --abbrev-ref HEAD"); - this.cached_branch = git.StartAndReadStandardOutput (); - - return this.cached_branch; - } - } - bool in_merge { get { - string merge_file_path = Path.Combine (LocalPath, ".git", "MERGE_HEAD"); + string merge_file_path = Path.Combine (LocalPath, ".svn", "MERGE_HEAD"); return File.Exists (merge_file_path); } } @@ -72,19 +41,13 @@ bool in_merge { public SubversionRepository (string path, Configuration config, SSHAuthenticationInfo auth_info) : base (path, config) { this.auth_info = auth_info; - - var git_config = new SubversionCommand (LocalPath, "config core.ignorecase false"); - git_config.StartAndWaitForExit (); - - git_config = new SubversionCommand (LocalPath, "config remote.origin.url \"" + RemoteUrl + "\""); - git_config.StartAndWaitForExit (); } public override List ExcludePaths { get { List rules = new List (); - rules.Add (".git"); + rules.Add (".svn"); return rules; } @@ -93,53 +56,26 @@ public override List ExcludePaths { public override double Size { get { - string file_path = Path.Combine (LocalPath, ".git", "info", "size"); - - try { - string size = File.ReadAllText (file_path); - return double.Parse (size); - - } catch { - return 0; - } + // TODO + return 0; } } public override double HistorySize { get { - string file_path = Path.Combine (LocalPath, ".git", "info", "history_size"); - - try { - string size = File.ReadAllText (file_path); - return double.Parse (size); - - } catch { - return 0; - } + // TODO + return 0; } } - void UpdateSizes () - { - double size = CalculateSizes (new DirectoryInfo (LocalPath)); - double history_size = CalculateSizes (new DirectoryInfo (Path.Combine (LocalPath, ".git"))); - - string size_file_path = Path.Combine (LocalPath, ".git", "info", "size"); - string history_size_file_path = Path.Combine (LocalPath, ".git", "info", "history_size"); - - File.WriteAllText (size_file_path, size.ToString ()); - File.WriteAllText (history_size_file_path, history_size.ToString ()); - } - - public override string CurrentRevision { get { - var git = new SubversionCommand (LocalPath, "rev-parse HEAD"); - string output = git.StartAndReadStandardOutput (); + var svn = new SubversionCommand (LocalPath, "info --show-item revision"); + string output = svn.StartAndReadStandardOutput (); - if (git.ExitCode == 0) + if (svn.ExitCode == 0) return output; return null; @@ -149,37 +85,24 @@ public override string CurrentRevision { public override bool HasRemoteChanges { get { - Logger.LogInfo ("Git", Name + " | Checking for remote changes..."); + Logger.LogInfo ("Svn", Name + " | Checking for remote changes..."); string current_revision = CurrentRevision; - var git = new SubversionCommand (LocalPath, - "ls-remote --heads --exit-code origin " + this.branch, auth_info); + var svn = new SubversionCommand (LocalPath, + "info --show-item revision " + RemoteUrl, auth_info); - string output = git.StartAndReadStandardOutput (); + string output = svn.StartAndReadStandardOutput (); - if (git.ExitCode != 0) + if (svn.ExitCode != 0) return false; - string remote_revision = "" + output.Substring (0, 40); + string remote_revision = output; if (!remote_revision.Equals (current_revision)) { - git = new SubversionCommand (LocalPath, "merge-base " + remote_revision + " master"); - git.StartAndWaitForExit (); - - if (git.ExitCode != 0) { - Logger.LogInfo ("Git", Name + " | Remote changes found, local: " + - current_revision + ", remote: " + remote_revision); - - Error = ErrorStatus.None; - return true; - - } else { - Logger.LogInfo ("Git", Name + " | Remote " + remote_revision + " is already in our history"); - return false; - } + return true; } - Logger.LogInfo ("Git", Name + " | No remote changes, local+remote: " + current_revision); + Logger.LogInfo ("Svn", Name + " | No remote changes, local+remote: " + current_revision); return false; } } @@ -200,38 +123,16 @@ public override bool SyncUp () if (message != null) Commit (message); - string pre_push_hook_path = Path.Combine (LocalPath, ".git", "hooks", "pre-push"); - string pre_push_hook_content; - - // The pre-push hook may have been changed by Git LFS, overwrite it to use our own configuration - if (InstallationInfo.OperatingSystem == OS.Mac) { - pre_push_hook_content = - "#!/bin/sh" + Environment.NewLine + - "env GIT_SSH_COMMAND='" + SubversionCommand.FormatGitSSHCommand (auth_info) + "' " + - Path.Combine (Configuration.DefaultConfiguration.BinPath, "git-lfs") + " pre-push \"$@\""; - - } else { - pre_push_hook_content = - "#!/bin/sh" + Environment.NewLine + - "env GIT_SSH_COMMAND='" + SubversionCommand.FormatGitSSHCommand (auth_info) + "' " + - "git-lfs pre-push \"$@\""; - } - - Directory.CreateDirectory (Path.GetDirectoryName (pre_push_hook_path)); - File.WriteAllText (pre_push_hook_path, pre_push_hook_content); - - var git_push = new SubversionCommand (LocalPath, string.Format ("push --all --progress origin", RemoteUrl), auth_info); - git_push.StartInfo.RedirectStandardError = true; - git_push.Start (); + var svn_push = new SubversionCommand (LocalPath, string.Format ("commit", RemoteUrl), auth_info); + svn_push.StartInfo.RedirectStandardError = true; + svn_push.Start (); - if (!ReadStream (git_push)) + if (!ReadStream (svn_push)) return false; - git_push.WaitForExit (); - - UpdateSizes (); + svn_push.WaitForExit (); - if (git_push.ExitCode == 0) + if (svn_push.ExitCode == 0) return true; Error = ErrorStatus.HostUnreachable; @@ -241,46 +142,23 @@ public override bool SyncUp () public override bool SyncDown () { - string lfs_is_behind_file_path = Path.Combine (LocalPath, ".git", "lfs", "is_behind"); - - if (StorageType == StorageType.LargeFiles) - File.Create (lfs_is_behind_file_path); - var git_fetch = new SubversionCommand (LocalPath, "fetch --progress origin " + branch, auth_info); + var svn_up = new SubversionCommand (LocalPath, "up", auth_info); - git_fetch.StartInfo.RedirectStandardError = true; - git_fetch.Start (); + svn_up.StartInfo.RedirectStandardError = true; + svn_up.Start (); - if (!ReadStream (git_fetch)) + if (!ReadStream (svn_up)) return false; - git_fetch.WaitForExit (); + svn_up.WaitForExit (); - if (git_fetch.ExitCode != 0) { + if (svn_up.ExitCode != 0) { Error = ErrorStatus.HostUnreachable; return false; } - if (Merge ()) { - if (StorageType == StorageType.LargeFiles) { - // Pull LFS files manually to benefit from concurrency - var git_lfs_pull = new SubversionCommand (LocalPath, "lfs pull origin", auth_info); - git_lfs_pull.StartAndWaitForExit (); - - if (git_lfs_pull.ExitCode != 0) { - Error = ErrorStatus.HostUnreachable; - return false; - } - - if (File.Exists (lfs_is_behind_file_path)) - File.Delete (lfs_is_behind_file_path); - } - - UpdateSizes (); - return true; - } - - return false; + return true; } @@ -305,7 +183,7 @@ bool ReadStream (SubversionCommand command) command.Kill (); command.Dispose (); - Logger.LogInfo ("Git", Name + " | Error status changed to " + Error); + Logger.LogInfo ("Svn", Name + " | Error status changed to " + Error); return false; } @@ -321,8 +199,8 @@ public override bool HasLocalChanges { get { PrepareDirectories (LocalPath); - var git = new SubversionCommand (LocalPath, "status --porcelain"); - string output = git.StartAndReadStandardOutput (); + var svn = new SubversionCommand (LocalPath, "status"); + string output = svn.StartAndReadStandardOutput (); return !string.IsNullOrEmpty (output); } @@ -330,253 +208,37 @@ public override bool HasLocalChanges { public override bool HasUnsyncedChanges { + // TODO get { - if (StorageType == StorageType.LargeFiles) { - string lfs_is_behind_file_path = Path.Combine (LocalPath, ".git", "lfs", "is_behind"); - - if (File.Exists (lfs_is_behind_file_path)) - return true; - } - - string unsynced_file_path = Path.Combine (LocalPath, ".git", "has_unsynced_changes"); - return File.Exists (unsynced_file_path); + return false; } - set { - string unsynced_file_path = Path.Combine (LocalPath, ".git", "has_unsynced_changes"); - - if (value) - File.WriteAllText (unsynced_file_path, ""); - else - File.Delete (unsynced_file_path); } } - // Stages the made changes bool Add () { - var git = new SubversionCommand (LocalPath, "add --all"); - git.StartAndWaitForExit (); + var svn = new SubversionCommand (LocalPath, "add --depth infinity -q *"); + string output = svn.StartAndReadStandardError() + .Replace("svn: E200009: Could not add all targets because some targets are already versioned", "") + .Replace ("svn: E200009: Illegal target for the requested operation", ""); - return (git.ExitCode == 0); + return !string.IsNullOrEmpty (output); } // Commits the made changes void Commit (string message) { - SubversionCommand git; + SubversionCommand svn; - if (!this.user_is_set) { - git = new SubversionCommand (LocalPath, "config user.name \"" + base.local_config.User.Name + "\""); - git.StartAndWaitForExit (); + svn = new SubversionCommand (LocalPath, "commit -m=\"" + message + "\" " + + "--username=\"" + base.local_config.User.Name + "\""); - git = new SubversionCommand (LocalPath, "config user.email \"" + base.local_config.User.Email + "\""); - git.StartAndWaitForExit (); - - this.user_is_set = true; - } - - git = new SubversionCommand (LocalPath, "commit --all --message=\"" + message + "\" " + - "--author=\"" + base.local_config.User.Name + " <" + base.local_config.User.Email + ">\""); - - git.StartAndReadStandardOutput (); + svn.StartAndReadStandardOutput (); } - - // Merges the fetched changes - bool Merge () - { - string message = FormatCommitMessage (); - - if (message != null) { - Add (); - Commit (message); - } - - SubversionCommand git; - - // Stop if we're already in a merge because something went wrong - if (this.in_merge) { - git = new SubversionCommand (LocalPath, "merge --abort"); - git.StartAndWaitForExit (); - - return false; - } - - // Temporarily change the ignorecase setting to true to avoid - // conflicts in file names due to letter case changes - git = new SubversionCommand (LocalPath, "config core.ignorecase true"); - git.StartAndWaitForExit (); - - git = new SubversionCommand (LocalPath, "merge FETCH_HEAD"); - git.StartInfo.RedirectStandardOutput = false; - - string error_output = git.StartAndReadStandardError (); - - if (git.ExitCode != 0) { - // Stop when we can't merge due to locked local files - // error: cannot stat 'filename': Permission denied - if (error_output.Contains ("error: cannot stat")) { - Error = ErrorStatus.UnreadableFiles; - Logger.LogInfo ("Git", Name + " | Error status changed to " + Error); - - git = new SubversionCommand (LocalPath, "merge --abort"); - git.StartAndWaitForExit (); - - git = new SubversionCommand (LocalPath, "config core.ignorecase false"); - git.StartAndWaitForExit (); - - return false; - - } else { - Logger.LogInfo ("Git", error_output); - Logger.LogInfo ("Git", Name + " | Conflict detected, trying to get out..."); - - while (this.in_merge && HasLocalChanges) { - try { - ResolveConflict (); - - } catch (Exception e) { - Logger.LogInfo ("Git", Name + " | Failed to resolve conflict, trying again...", e); - } - } - - Logger.LogInfo ("Git", Name + " | Conflict resolved"); - } - } - - git = new SubversionCommand (LocalPath, "config core.ignorecase false"); - git.StartAndWaitForExit (); - - return true; - } - - - void ResolveConflict () - { - // This is a 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 server's, save ours as a timestamped copy - // UD unmerged, deleted by them -> Use ours - // UA unmerged, added by them -> Use server's, save ours as a timestamped copy - // DU unmerged, deleted by us -> Use server's - // AA unmerged, both added -> Use server's, save ours as a timestamped copy - // UU unmerged, both modified -> Use server's, save ours as a timestamped copy - // ?? unmerged, new files -> Stage the new files - - var git_status = new SubversionCommand (LocalPath, "status --porcelain"); - string output = git_status.StartAndReadStandardOutput (); - - string [] lines = output.Split ("\n".ToCharArray ()); - bool trigger_conflict_event = false; - - foreach (string line in lines) { - string conflicting_path = line.Substring (3); - conflicting_path = EnsureSpecialCharacters (conflicting_path); - conflicting_path = conflicting_path.Trim ("\"".ToCharArray ()); - - // Remove possible rename indicators - string [] separators = {" -> \"", " -> "}; - foreach (string separator in separators) { - if (conflicting_path.Contains (separator)) { - conflicting_path = conflicting_path.Substring ( - conflicting_path.IndexOf (separator) + separator.Length); - } - } - - Logger.LogInfo ("Git", Name + " | Conflict type: " + line); - - // Ignore conflicts in hidden files and use the local versions - if (conflicting_path.EndsWith (".sparkleshare") || conflicting_path.EndsWith (".empty")) { - Logger.LogInfo ("Git", Name + " | Ignoring conflict in special file: " + conflicting_path); - - // Recover local version - var git_ours = new SubversionCommand (LocalPath, "checkout --ours \"" + conflicting_path + "\""); - git_ours.StartAndWaitForExit (); - - string abs_conflicting_path = Path.Combine (LocalPath, conflicting_path); - - if (File.Exists (abs_conflicting_path)) - File.SetAttributes (abs_conflicting_path, FileAttributes.Hidden); - - continue; - } - - Logger.LogInfo ("Git", Name + " | Resolving: " + conflicting_path); - - // 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 - var git_ours = new SubversionCommand (LocalPath, "checkout --ours \"" + conflicting_path + "\""); - git_ours.StartAndWaitForExit (); - - // Append a timestamp to local version. - // Windows doesn't allow colons in the file name, so - // we use "h" between the hours and minutes instead. - string timestamp = DateTime.Now.ToString ("MMM d H\\hmm"); - string our_path = Path.GetFileNameWithoutExtension (conflicting_path) + - " (" + base.local_config.User.Name + ", " + timestamp + ")" + Path.GetExtension (conflicting_path); - - string abs_conflicting_path = Path.Combine (LocalPath, conflicting_path); - string abs_our_path = Path.Combine (LocalPath, our_path); - - if (File.Exists (abs_conflicting_path) && !File.Exists (abs_our_path)) - File.Move (abs_conflicting_path, abs_our_path); - - // Recover server version - var git_theirs = new SubversionCommand (LocalPath, "checkout --theirs \"" + conflicting_path + "\""); - git_theirs.StartAndWaitForExit (); - - trigger_conflict_event = true; - - - // The server version has been modified, but the local version was removed - } else 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 - var git_add = new SubversionCommand (LocalPath, "add \"" + conflicting_path + "\""); - git_add.StartAndWaitForExit (); - - - // The local version has been modified, but the server version was removed - } else if (line.StartsWith ("UD")) { - - // Recover server version - var git_theirs = new SubversionCommand (LocalPath, "checkout --theirs \"" + conflicting_path + "\""); - git_theirs.StartAndWaitForExit (); - - - // Server and local versions were removed - } else if (line.StartsWith ("DD")) { - Logger.LogInfo ("Git", Name + " | No need to resolve: " + line); - - // New local files - } else if (line.StartsWith ("??")) { - Logger.LogInfo ("Git", Name + " | Found new file, no need to resolve: " + line); - - } else { - Logger.LogInfo ("Git", Name + " | Don't know what to do with: " + line); - } - } - - Add (); - - var git = new SubversionCommand (LocalPath, "commit --message \"Conflict resolution by SparkleShare\""); - git.StartInfo.RedirectStandardOutput = false; - git.StartAndWaitForExit (); - - if (trigger_conflict_event) - OnConflictResolved (); - } - - public override void RestoreFile (string path, string revision, string target_file_path) { if (path == null) @@ -585,11 +247,11 @@ public override void RestoreFile (string path, string revision, string target_fi if (revision == null) throw new ArgumentNullException ("revision"); - Logger.LogInfo ("Git", Name + " | Restoring \"" + path + "\" (revision " + revision + ")"); + Logger.LogInfo ("Svn", Name + " | Restoring \"" + path + "\" (revision " + revision + ")"); // Restore the older file... - var git = new SubversionCommand (LocalPath, "checkout " + revision + " \"" + path + "\""); - git.StartAndWaitForExit (); + var svn = new SubversionCommand (LocalPath, "checkout -r" + revision + " \"" + path + "\""); + svn.StartAndWaitForExit (); string local_file_path = Path.Combine (LocalPath, path); @@ -598,13 +260,13 @@ public override void RestoreFile (string path, string revision, string target_fi File.Move (local_file_path, target_file_path); } catch { - Logger.LogInfo ("Git", + Logger.LogInfo ("Svn", Name + " | Could not move \"" + local_file_path + "\" to \"" + target_file_path + "\""); } // ...and restore the most recent revision - git = new SubversionCommand (LocalPath, "checkout " + CurrentRevision + " \"" + path + "\""); - git.StartAndWaitForExit (); + svn = new SubversionCommand (LocalPath, "checkout -r" + CurrentRevision + " \"" + path + "\""); + svn.StartAndWaitForExit (); if (target_file_path.StartsWith (LocalPath)) @@ -632,56 +294,31 @@ public override List GetChangeSets (string path) List GetChangeSetsInternal (string path) { var change_sets = new List (); - SubversionCommand git; - - if (path == null) { - git = new SubversionCommand (LocalPath, "--no-pager log --since=1.month --raw --find-renames --date=iso " + - "--format=medium --no-color --no-merges"); - - } else { - path = path.Replace ("\\", "/"); - - git = new SubversionCommand (LocalPath, "--no-pager log --raw --find-renames --date=iso " + - "--format=medium --no-color --no-merges -- \"" + path + "\""); - } + SubversionCommand svn; - string output = git.StartAndReadStandardOutput (); + svn = new SubversionCommand (LocalPath, "log -l 100"); - if (path == null && string.IsNullOrWhiteSpace (output)) { - git = new SubversionCommand (LocalPath, "--no-pager log -n 75 --raw --find-renames --date=iso " + - "--format=medium --no-color --no-merges"); - - output = git.StartAndReadStandardOutput (); - } + string output = svn.StartAndReadStandardOutput (); string [] lines = output.Split ("\n".ToCharArray ()); List entries = new List (); // Split up commit entries int line_number = 0; - bool first_pass = true; - string entry = "", last_entry = ""; - foreach (string line in lines) { - if (line.StartsWith ("commit") && !first_pass) { - entries.Add (entry); - entry = ""; - line_number = 0; - - } else { - first_pass = false; - } - - // Only parse first 250 files to prevent memory issues - if (line_number < 250) { - entry += line + "\n"; - line_number++; + 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; } - - last_entry = entry; } - entries.Add (last_entry); - // Parse commit entries foreach (string log_entry in entries) { Match match = this.log_regex.Match (log_entry); @@ -896,15 +533,15 @@ void PrepareDirectories (string path) if (IsSymlink (child_path)) continue; - if (child_path.EndsWith (".git")) { - if (child_path.Equals (Path.Combine (LocalPath, ".git"))) + 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 ("Git", Name + " | Renamed " + HEAD_file_path); + Logger.LogInfo ("Svn", Name + " | Renamed " + HEAD_file_path); } continue; @@ -923,13 +560,13 @@ void PrepareDirectories (string path) File.SetAttributes (Path.Combine (path, ".empty"), FileAttributes.Hidden); } catch { - Logger.LogInfo ("Git", Name + " | Failed adding empty folder " + path); + Logger.LogInfo ("Svn", Name + " | Failed adding empty folder " + path); } } } } catch (IOException e) { - Logger.LogInfo ("Git", "Failed preparing directory", e); + Logger.LogInfo ("Svn", "Failed preparing directory", e); } } @@ -939,11 +576,11 @@ List ParseStatus () { List changes = new List (); - var git_status = new SubversionCommand (LocalPath, "status --porcelain"); - git_status.Start (); + var svn_status = new SubversionCommand (LocalPath, "status --porcelain"); + svn_status.Start (); - while (!git_status.StandardOutput.EndOfStream) { - string line = git_status.StandardOutput.ReadLine (); + while (!svn_status.StandardOutput.EndOfStream) { + string line = svn_status.StandardOutput.ReadLine (); line = line.Trim (); if (line.EndsWith (".empty") || line.EndsWith (".empty\"")) @@ -977,8 +614,8 @@ List ParseStatus () changes.Add (change); } - git_status.StandardOutput.ReadToEnd (); - git_status.WaitForExit (); + svn_status.StandardOutput.ReadToEnd (); + svn_status.WaitForExit (); return changes; } @@ -1026,8 +663,7 @@ long CalculateSizes (DirectoryInfo parent) try { foreach (DirectoryInfo directory in parent.GetDirectories ()) { if (directory.FullName.IsSymlink () || - directory.Name.Equals (".git") || - directory.Name.Equals ("rebase-apply")) { + directory.Name.Equals (".svn")) { continue; } From b93035fbe7c4bdd652ab136e81e902450f024c3d Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Thu, 25 Aug 2016 08:40:31 -0400 Subject: [PATCH 4/6] svn not git --- Sparkles/Subversion/SubversionRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sparkles/Subversion/SubversionRepository.cs b/Sparkles/Subversion/SubversionRepository.cs index 4522d11ac..2a201a3b0 100644 --- a/Sparkles/Subversion/SubversionRepository.cs +++ b/Sparkles/Subversion/SubversionRepository.cs @@ -576,7 +576,7 @@ List ParseStatus () { List changes = new List (); - var svn_status = new SubversionCommand (LocalPath, "status --porcelain"); + var svn_status = new SubversionCommand (LocalPath, "status"); svn_status.Start (); while (!svn_status.StandardOutput.EndOfStream) { From 672db7ebd4f5c5daf13b15a55d1da09c57d2d0dc Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Sun, 28 Aug 2016 12:34:14 +0100 Subject: [PATCH 5/6] svn-add to push back new files works --- Sparkles/Subversion/SubversionCommand.cs | 47 +-------------- Sparkles/Subversion/SubversionFetcher.cs | 10 ---- Sparkles/Subversion/SubversionRepository.cs | 66 +++++++-------------- 3 files changed, 24 insertions(+), 99 deletions(-) diff --git a/Sparkles/Subversion/SubversionCommand.cs b/Sparkles/Subversion/SubversionCommand.cs index 19d858277..bdcad553d 100644 --- a/Sparkles/Subversion/SubversionCommand.cs +++ b/Sparkles/Subversion/SubversionCommand.cs @@ -63,59 +63,14 @@ public SubversionCommand (string working_dir, string args) : this (working_dir, 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 Regex progress_regex = new Regex (@"([0-9]+)%", RegexOptions.Compiled); - static Regex speed_regex = new Regex (@"([0-9\.]+) ([KM])iB/s", RegexOptions.Compiled); - - public static ErrorStatus ParseProgress (string line, out double percentage, out double speed, out string information) - { - percentage = 0; - speed = 0; - information = ""; - - Match match; - - match = progress_regex.Match (line); - - if (!match.Success || string.IsNullOrWhiteSpace (line)) { - if (!string.IsNullOrWhiteSpace (line)) - Logger.LogInfo ("Svn", line); - - return FindError (line); - } - - int number = int.Parse (match.Groups [1].Value); - - // The transfer process consists of two stages: the "Compressing - // objects" stage which we count as 20% of the total progress, and - // the "Writing objects" stage which we count as the last 80% - if (line.Contains ("Compressing objects")) { - // "Compressing objects" stage - percentage = (number / 100 * 20); - - } else if (line.Contains ("Writing objects")) { - percentage = (number / 100 * 80 + 20); - Match speed_match = speed_regex.Match (line); - - if (speed_match.Success) { - speed = double.Parse (speed_match.Groups [1].Value, new CultureInfo ("en-US")) * 1024; - - if (speed_match.Groups [2].Value.Equals ("M")) - speed = speed * 1024; - - information = speed.ToSize (); - } - } - return ErrorStatus.None; } diff --git a/Sparkles/Subversion/SubversionFetcher.cs b/Sparkles/Subversion/SubversionFetcher.cs index 9ddf35cc7..16c1bf1e4 100644 --- a/Sparkles/Subversion/SubversionFetcher.cs +++ b/Sparkles/Subversion/SubversionFetcher.cs @@ -89,16 +89,6 @@ public override bool Fetch () while (!output_stream.EndOfStream) { string line = output_stream.ReadLine (); - ErrorStatus error = SubversionCommand.ParseProgress (line, out percentage, out speed, out information); - - if (error != ErrorStatus.None) { - IsActive = false; - svn_checkout.Kill (); - svn_checkout.Dispose (); - - return false; - } - OnProgressChanged (percentage, speed, information); } diff --git a/Sparkles/Subversion/SubversionRepository.cs b/Sparkles/Subversion/SubversionRepository.cs index 2a201a3b0..38b091404 100644 --- a/Sparkles/Subversion/SubversionRepository.cs +++ b/Sparkles/Subversion/SubversionRepository.cs @@ -120,19 +120,16 @@ public override bool SyncUp () if (string.IsNullOrEmpty (message)) message = FormatCommitMessage (); - if (message != null) - Commit (message); + var svn_commit = new SubversionCommand (LocalPath, string.Format ("commit -m=\"" + message + "\" ", RemoteUrl), auth_info); + svn_commit.StartInfo.RedirectStandardError = true; + svn_commit.Start (); - var svn_push = new SubversionCommand (LocalPath, string.Format ("commit", RemoteUrl), auth_info); - svn_push.StartInfo.RedirectStandardError = true; - svn_push.Start (); - - if (!ReadStream (svn_push)) + if (!ReadStream (svn_commit)) return false; - svn_push.WaitForExit (); + svn_commit.WaitForExit (); - if (svn_push.ExitCode == 0) + if (svn_commit.ExitCode == 0) return true; Error = ErrorStatus.HostUnreachable; @@ -164,31 +161,15 @@ public override bool SyncDown () bool ReadStream (SubversionCommand command) { - StreamReader output_stream = command.StandardError; - if (StorageType == StorageType.LargeFiles) - output_stream = command.StandardOutput; + StreamReader output_stream = command.StandardError; - double percentage = 0; - double speed = 0; string information = ""; while (!output_stream.EndOfStream) { string line = output_stream.ReadLine (); - ErrorStatus error = SubversionCommand.ParseProgress (line, out percentage, out speed, out information); - - if (error != ErrorStatus.None) { - Error = error; - information = line; - - command.Kill (); - command.Dispose (); - Logger.LogInfo ("Svn", Name + " | Error status changed to " + Error); - return false; - } - - OnProgressChanged (percentage, speed, information); + OnProgressChanged (0, 0, information); } return true; @@ -219,26 +200,24 @@ public override bool HasUnsyncedChanges { // Stages the made changes bool Add () { - var svn = new SubversionCommand (LocalPath, "add --depth infinity -q *"); - string output = svn.StartAndReadStandardError() - .Replace("svn: E200009: Could not add all targets because some targets are already versioned", "") - .Replace ("svn: E200009: Illegal target for the requested operation", ""); - - return !string.IsNullOrEmpty (output); - } - - - // Commits the made changes - void Commit (string message) - { - SubversionCommand svn; - svn = new SubversionCommand (LocalPath, "commit -m=\"" + message + "\" " + - "--username=\"" + base.local_config.User.Name + "\""); + 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++; + } + } + } - svn.StartAndReadStandardOutput (); + return count > 0; } + public override void RestoreFile (string path, string revision, string target_file_path) { if (path == null) @@ -581,6 +560,7 @@ List ParseStatus () while (!svn_status.StandardOutput.EndOfStream) { string line = svn_status.StandardOutput.ReadLine (); + line = line.Trim (); if (line.EndsWith (".empty") || line.EndsWith (".empty\"")) From 2ec187126c26a04fb98a7d3dd4b98bd15522ccbd Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Sun, 28 Aug 2016 20:06:02 +0100 Subject: [PATCH 6/6] Mods and Deletes too --- Sparkles/Subversion/SubversionRepository.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Sparkles/Subversion/SubversionRepository.cs b/Sparkles/Subversion/SubversionRepository.cs index 38b091404..d3eb695df 100644 --- a/Sparkles/Subversion/SubversionRepository.cs +++ b/Sparkles/Subversion/SubversionRepository.cs @@ -208,9 +208,16 @@ bool Add () string line; while ((line = reader.ReadLine ()) != null) { if (line.StartsWith ("? ")) { - new SubversionCommand (LocalPath, "add " + line.Substring(7)).Start(); + 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 } } @@ -550,21 +557,18 @@ void PrepareDirectories (string path) } - List ParseStatus () { List changes = new List (); - var svn_status = new SubversionCommand (LocalPath, "status"); + 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 (); - - if (line.EndsWith (".empty") || line.EndsWith (".empty\"")) - line = line.Replace (".empty", ""); + Console.WriteLine ("LL " + line); Change change;