public PryanetRepo(string path, PryanetConfig config) : base(path, config) { PryanetGit git = new PryanetGit (LocalPath, "config core.ignorecase false"); git.StartAndWaitForExit (); // Check if we should use git-bin git = new PryanetGit (LocalPath, "config --get filter.bin.clean"); git.StartAndWaitForExit (); this.use_git_bin = (git.ExitCode == 0); if (this.use_git_bin) ConfigureGitBin (); git = new PryanetGit (LocalPath, "config remote.origin.url \"" + RemoteUrl + "\""); git.StartAndWaitForExit (); string password_file_path = Path.Combine (LocalPath, ".git", "password"); if (File.Exists (password_file_path)) this.is_encrypted = true; }
// Merges the fetched changes private bool Rebase() { string message = FormatCommitMessage (); if (message != null) { Add (); Commit (message); } PryanetGit git; string rebase_apply_path = new string [] { LocalPath, ".git", "rebase-apply" }.Combine (); // Stop if we're already in a rebase because something went wrong if (Directory.Exists (rebase_apply_path)) { git = new PryanetGit (LocalPath, "rebase --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 PryanetGit (LocalPath, "config core.ignorecase true"); git.StartAndWaitForExit (); git = new PryanetGit (LocalPath, "rebase FETCH_HEAD"); git.StartInfo.RedirectStandardOutput = false; string error_output = git.StartAndReadStandardError (); if (git.ExitCode != 0) { // Stop when we can't rebase due to locked local files // error: cannot stat 'filename': Permission denied if (error_output.Contains ("error: cannot stat")) { Error = ErrorStatus.UnreadableFiles; PryanetLogger.LogInfo ("Git", Name + " | Error status changed to " + Error); git = new PryanetGit (LocalPath, "rebase --abort"); git.StartAndWaitForExit (); git = new PryanetGit (LocalPath, "config core.ignorecase false"); git.StartAndWaitForExit (); return false; } else { PryanetLogger.LogInfo ("", error_output); PryanetLogger.LogInfo ("Git", Name + " | Conflict detected, trying to get out..."); while (Directory.Exists (rebase_apply_path) && HasLocalChanges) { try { ResolveConflict (); } catch (IOException e) { PryanetLogger.LogInfo ("Git", Name + " | Failed to resolve conflict, trying again...", e); } } PryanetLogger.LogInfo ("Git", Name + " | Conflict resolved"); OnConflictResolved (); } } git = new PryanetGit (LocalPath, "config core.ignorecase false"); git.StartAndWaitForExit (); return true; }
private List<PryanetChangeSet> GetChangeSetsInternal(string path) { List <PryanetChangeSet> change_sets = new List <PryanetChangeSet> (); PryanetGit git; if (path == null) { git = new PryanetGit (LocalPath, "log --since=1.month --raw --find-renames --date=iso " + "--format=medium --no-color --no-merges"); } else { path = path.Replace ("\\", "/"); git = new PryanetGit (LocalPath, "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 PryanetGit (LocalPath, "log -n 75 --raw --find-renames --date=iso " + "--format=medium --no-color --no-merges"); output = git.StartAndReadStandardOutput (); } string [] lines = output.Split ("\n".ToCharArray ()); List<string> entries = new List <string> (); // 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) continue; PryanetChangeSet change_set = new PryanetChangeSet (); change_set.Folder = new PryanetFolder (Name); change_set.Revision = match.Groups [1].Value; change_set.User = new PryanetUser (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 (".pryanetshare")) 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) { PryanetLogger.LogInfo ("Local", "Error parsing file name '" + file_path + "'", e); continue; } file_path = file_path.Replace ("\\\"", "\""); PryanetChange change = new PryanetChange () { Path = file_path, IsFolder = change_is_folder, Timestamp = change_set.Timestamp, Type = PryanetChangeType.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) { PryanetLogger.LogInfo ("Local", "Error parsing file name '" + file_path + "'", e); continue; } try { to_file_path = EnsureSpecialCharacters (to_file_path); } catch (Exception e) { PryanetLogger.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 = PryanetChangeType.Moved; } else if (type_letter.Equals ("M")) { change.Type = PryanetChangeType.Edited; } else if (type_letter.Equals ("D")) { change.Type = PryanetChangeType.Deleted; } change_set.Changes.Add (change); } // Group commits per user, per day if (change_sets.Count > 0 && path == null) { PryanetChangeSet 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<PryanetChange> changes_to_skip = new List<PryanetChange> (); foreach (PryanetChange change in change_set.Changes) { if ((change.Type == PryanetChangeType.Deleted || change.Type == PryanetChangeType.Moved) && change.Path.Equals (path)) { changes_to_skip.Add (change); } } foreach (PryanetChange change_to_skip in changes_to_skip) change_set.Changes.Remove (change_to_skip); } change_sets.Add (change_set); } } return change_sets; }
// Creates a pretty commit message based on what has changed private string FormatCommitMessage() { int count = 0; string message = ""; PryanetGit git_status = new PryanetGit (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", ""); 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 ()); message += "< ‘" + EnsureSpecialCharacters (path) + "’\n"; message += "> ‘" + EnsureSpecialCharacters (moved_to_path) + "’\n"; } else { if (line.StartsWith ("M")) { message += "/"; } else if (line.StartsWith ("D")) { message += "-"; } else { message += "+"; } string path = line.Substring (3).Trim ("\"".ToCharArray ()); message += " ‘" + EnsureSpecialCharacters (path) + "’\n"; } count++; if (count == 10) { message += "...\n"; break; } } git_status.StandardOutput.ReadToEnd (); git_status.WaitForExit (); if (string.IsNullOrWhiteSpace (message)) return null; else return message; }
private void ConfigureGitBin() { PryanetGit git = new PryanetGit (LocalPath, "config filter.bin.clean \"git bin clean %f\""); git.StartAndWaitForExit (); git = new PryanetGit (LocalPath, "config filter.bin.smudge \"git bin smudge\""); git.StartAndWaitForExit (); git = new PryanetGit (LocalPath, "config git-bin.sftpUrl \"" + RemoteUrl + "\""); git.StartAndWaitForExit (); git = new PryanetGit (LocalPath, "config git-bin.sftpPrivateKeyFile \"" + base.local_config.User.PrivateKeyFilePath + "\""); git.StartAndWaitForExit (); }
// Commits the made changes private void Commit(string message) { PryanetGit git; if (!this.user_is_set) { git = new PryanetGit (LocalPath, "config user.name \"" + base.local_config.User.Name + "\""); git.StartAndWaitForExit (); git = new PryanetGit (LocalPath, "config user.email \"" + base.local_config.User.Email + "\""); git.StartAndWaitForExit (); this.user_is_set = true; } git = new PryanetGit (LocalPath, "commit --all --message=\"" + message + "\" " + "--author=\"" + base.local_config.User.Name + " <" + base.local_config.User.Email + ">\""); git.StartAndReadStandardOutput (); }
public override bool SyncUp() { if (!Add ()) { Error = ErrorStatus.UnreadableFiles; return false; } string message = FormatCommitMessage (); if (message != null) Commit (message); if (this.use_git_bin) { PryanetGitBin git_bin = new PryanetGitBin (LocalPath, "push"); git_bin.StartAndWaitForExit (); // TODO: Progress } PryanetGit git = new PryanetGit (LocalPath, "push --progress \"" + RemoteUrl + "\" " + this.branch); git.StartInfo.RedirectStandardError = true; git.Start (); double percentage = 1.0; while (!git.StandardError.EndOfStream) { string line = git.StandardError.ReadLine (); Match match = this.progress_regex.Match (line); double speed = 0.0; double number = 0.0; if (match.Success) { try { number = double.Parse (match.Groups [1].Value, new CultureInfo ("en-US")); } catch (FormatException) { PryanetLogger.LogInfo ("Git", "Error parsing progress: \"" + match.Groups [1] + "\""); } // The pushing progress 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.StartsWith ("Compressing")) { // "Compressing objects" stage number = (number / 100 * 20); } else { // "Writing objects" stage number = (number / 100 * 80 + 20); Match speed_match = this.speed_regex.Match (line); if (speed_match.Success) { try { speed = double.Parse (speed_match.Groups [1].Value, new CultureInfo ("en-US")) * 1024; } catch (FormatException) { PryanetLogger.LogInfo ("Git", "Error parsing speed: \"" + speed_match.Groups [1] + "\""); } if (speed_match.Groups [2].Value.Equals ("M")) speed = speed * 1024; } } } else { PryanetLogger.LogInfo ("Git", Name + " | " + line); if (FindError (line)) return false; } if (number >= percentage) { percentage = number; base.OnProgressChanged (percentage, speed); } } git.WaitForExit (); UpdateSizes (); if (git.ExitCode == 0) { ClearCache (); string salt_file_path = new string [] { LocalPath, ".git", "salt" }.Combine (); // If the repo is encrypted, create a branch to // store the salt in and push it to the host if (File.Exists (salt_file_path)) { string salt = File.ReadAllText (salt_file_path).Trim (); PryanetGit git_salt = new PryanetGit (LocalPath, "branch salt-" + salt); git_salt.StartAndWaitForExit (); git_salt = new PryanetGit (LocalPath, "push origin salt-" + salt); git_salt.StartAndWaitForExit (); File.Delete (salt_file_path); } return true; } else { Error = ErrorStatus.HostUnreachable; return false; } }
// Stages the made changes private bool Add() { PryanetGit git = new PryanetGit (LocalPath, "add --all"); git.StartAndWaitForExit (); return (git.ExitCode == 0); }
public override void RestoreFile(string path, string revision, string target_file_path) { if (path == null) throw new ArgumentNullException ("path"); if (revision == null) throw new ArgumentNullException ("revision"); PryanetLogger.LogInfo ("Git", Name + " | Restoring \"" + path + "\" (revision " + revision + ")"); // git-show doesn't decrypt objects, so we can't use it to retrieve // files from the index. This is a suboptimal workaround but it does the job if (this.is_encrypted) { // Restore the older file... PryanetGit git = new PryanetGit (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 { PryanetLogger.LogInfo ("Git", Name + " | Could not move \"" + local_file_path + "\" to \"" + target_file_path + "\""); } // ...and restore the most recent revision git = new PryanetGit (LocalPath, "checkout " + CurrentRevision + " \"" + path + "\""); git.StartAndWaitForExit (); // The correct way } else { path = path.Replace ("\"", "\\\""); PryanetGit git = new PryanetGit (LocalPath, "show " + revision + ":\"" + path + "\""); git.Start (); FileStream stream = File.OpenWrite (target_file_path); git.StandardOutput.BaseStream.CopyTo (stream); stream.Close (); git.WaitForExit (); } if (target_file_path.StartsWith (LocalPath)) new Thread (() => OnFileActivity (null)).Start (); }
public override bool SyncDown() { PryanetGit git = new PryanetGit (LocalPath, "fetch --progress \"" + RemoteUrl + "\" " + this.branch); git.StartInfo.RedirectStandardError = true; git.Start (); double percentage = 1.0; while (!git.StandardError.EndOfStream) { string line = git.StandardError.ReadLine (); Match match = this.progress_regex.Match (line); double speed = 0.0; double number = 0.0; if (match.Success) { try { number = double.Parse (match.Groups [1].Value, new CultureInfo ("en-US")); } catch (FormatException) { PryanetLogger.LogInfo ("Git", "Error parsing progress: \"" + match.Groups [1] + "\""); } // The fetching progress consists of two stages: the "Compressing // objects" stage which we count as 20% of the total progress, and // the "Receiving objects" stage which we count as the last 80% if (line.StartsWith ("Compressing")) { // "Compressing objects" stage number = (number / 100 * 20); } else { // "Writing objects" stage number = (number / 100 * 80 + 20); Match speed_match = this.speed_regex.Match (line); if (speed_match.Success) { try { speed = double.Parse (speed_match.Groups [1].Value, new CultureInfo ("en-US")) * 1024; } catch (FormatException) { PryanetLogger.LogInfo ("Git", "Error parsing speed: \"" + speed_match.Groups [1] + "\""); } if (speed_match.Groups [2].Value.Equals ("M")) speed = speed * 1024; } } } else { PryanetLogger.LogInfo ("Git", Name + " | " + line); if (FindError (line)) return false; } if (number >= percentage) { percentage = number; base.OnProgressChanged (percentage, speed); } } git.WaitForExit (); UpdateSizes (); if (git.ExitCode == 0) { if (Rebase ()) { ClearCache (); return true; } else { return false; } } else { Error = ErrorStatus.HostUnreachable; return false; } }
private void ResolveConflict() { // This is a list of conflict status codes that Git uses, their // meaning, and how PryanetShare 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 // // Note that a rebase merge works by replaying each commit from the working branch on // top of the upstream branch. Because of this, when a merge conflict happens the // side reported as 'ours' is the so-far rebased series, starting with upstream, // and 'theirs' is the working branch. In other words, the sides are swapped. // // So: 'ours' means the 'server's version' and 'theirs' means the 'local version' after this comment PryanetGit git_status = new PryanetGit (LocalPath, "status --porcelain"); string output = git_status.StartAndReadStandardOutput (); string [] lines = output.Split ("\n".ToCharArray ()); bool changes_added = false; foreach (string line in lines) { string conflicting_path = line.Substring (3); conflicting_path = EnsureSpecialCharacters (conflicting_path); conflicting_path = conflicting_path.Trim ("\"".ToCharArray ()); PryanetLogger.LogInfo ("Git", Name + " | Conflict type: " + line); // Ignore conflicts in the .pryanetshare file and use the local version if (conflicting_path.EndsWith (".pryanetshare") || conflicting_path.EndsWith (".empty")) { PryanetLogger.LogInfo ("Git", Name + " | Ignoring conflict in special file: " + conflicting_path); // Recover local version PryanetGit git_theirs = new PryanetGit (LocalPath, "checkout --theirs \"" + conflicting_path + "\""); git_theirs.StartAndWaitForExit (); File.SetAttributes (Path.Combine (LocalPath, conflicting_path), FileAttributes.Hidden); changes_added = true; continue; } PryanetLogger.LogInfo ("Git", Name + " | Resolving: " + line); // Both the local and server version have been modified if (line.StartsWith ("UU") || line.StartsWith ("AA") || line.StartsWith ("AU") || line.StartsWith ("UA")) { // Recover local version PryanetGit git_theirs = new PryanetGit (LocalPath, "checkout --theirs \"" + conflicting_path + "\""); git_theirs.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 their_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_their_path = Path.Combine (LocalPath, their_path); if (File.Exists (abs_conflicting_path) && !File.Exists (abs_their_path)) File.Move (abs_conflicting_path, abs_their_path); // Recover server version PryanetGit git_ours = new PryanetGit (LocalPath, "checkout --ours \"" + conflicting_path + "\""); git_ours.StartAndWaitForExit (); changes_added = true; // The local version has been modified, but the server 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 PryanetGit git_add = new PryanetGit (LocalPath, "add \"" + conflicting_path + "\""); git_add.StartAndWaitForExit (); changes_added = true; // The server version has been modified, but the local version was removed } else if (line.StartsWith ("UD")) { // Recover server version PryanetGit git_theirs = new PryanetGit (LocalPath, "checkout --ours \"" + conflicting_path + "\""); git_theirs.StartAndWaitForExit (); changes_added = true; // Server and local versions were removed } else if (line.StartsWith ("DD")) { PryanetLogger.LogInfo ("Git", Name + " | No need to resolve: " + line); // New local files } else if (line.StartsWith ("??")) { PryanetLogger.LogInfo ("Git", Name + " | Found new file, no need to resolve: " + line); changes_added = true; } else { PryanetLogger.LogInfo ("Git", Name + " | Don't know what to do with: " + line); } } Add (); PryanetGit git; if (changes_added) git = new PryanetGit (LocalPath, "rebase --continue"); else git = new PryanetGit (LocalPath, "rebase --skip"); git.StartInfo.RedirectStandardOutput = false; git.StartAndWaitForExit (); }
private void InstallConfiguration() { string [] settings = new string [] { "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.autocrlf false", // Don't change file line endings "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" }; foreach (string setting in settings) { PryanetGit git_config = new PryanetGit (TargetFolder, "config " + setting); git_config.StartAndWaitForExit (); } if (this.use_git_bin) InstallGitBinConfiguration (); }
public override bool IsFetchedRepoPasswordCorrect(string password) { string password_check_file_path = Path.Combine (TargetFolder, ".pryanetshare"); if (!File.Exists (password_check_file_path)) { PryanetGit git = new PryanetGit (TargetFolder, "show HEAD:.pryanetshare"); string output = git.StartAndReadStandardOutput (); if (git.ExitCode == 0) File.WriteAllText (password_check_file_path, output); else return false; } Process process = new Process (); process.EnableRaisingEvents = true; process.StartInfo.WorkingDirectory = TargetFolder; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.CreateNoWindow = true; process.StartInfo.FileName = "openssl"; process.StartInfo.Arguments = "enc -d -aes-256-cbc -base64 -S " + this.crypto_salt + " -pass pass:\"" + password + "\" -in \"" + password_check_file_path + "\""; PryanetLogger.LogInfo ("Cmd | " + System.IO.Path.GetFileName (process.StartInfo.WorkingDirectory), System.IO.Path.GetFileName (process.StartInfo.FileName) + " " + process.StartInfo.Arguments); process.Start (); process.WaitForExit (); if (process.ExitCode == 0) { File.Delete (password_check_file_path); return true; } else { return false; } }
public void InstallGitBinConfiguration() { string [] settings = new string [] { "core.bigFileThreshold 1024g", "filter.bin.clean \"git bin clean %f\"", "filter.bin.smudge \"git bin smudge\"" }; foreach (string setting in settings) { PryanetGit git_config = new PryanetGit (TargetFolder, "config " + setting); git_config.StartAndWaitForExit (); } }
public override void Complete() { if (!IsFetchedRepoEmpty) { PryanetGit git = new PryanetGit (TargetFolder, "checkout --quiet HEAD"); git.StartAndWaitForExit (); } base.Complete (); }