diff options
Diffstat (limited to 'SparkleLib/Git/SparkleRepoGit.cs')
-rw-r--r-- | SparkleLib/Git/SparkleRepoGit.cs | 556 |
1 files changed, 556 insertions, 0 deletions
diff --git a/SparkleLib/Git/SparkleRepoGit.cs b/SparkleLib/Git/SparkleRepoGit.cs new file mode 100644 index 0000000..3a611ca --- /dev/null +++ b/SparkleLib/Git/SparkleRepoGit.cs @@ -0,0 +1,556 @@ +// SparkleShare, a collaboration and sharing tool. +// Copyright (C) 2010 Hylke Bons <hylkebons@gmail.com> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text.RegularExpressions; + +namespace SparkleLib { + + public class SparkleRepoGit : SparkleRepoBase { + + public SparkleRepoGit (string path, SparkleBackend backend) : + base (path, backend) { } + + + public override string Identifier { + get { + + // Because git computes a hash based on content, + // author, and timestamp; it is unique enough to + // use the hash of the first commit as an identifier + // for our folder + SparkleGit git = new SparkleGit (LocalPath, "rev-list --reverse HEAD"); + git.Start (); + + // Reading the standard output HAS to go before + // WaitForExit, or it will hang forever on output > 4096 bytes + string output = git.StandardOutput.ReadToEnd (); + git.WaitForExit (); + + return output.Substring (0, 40); + } + } + + + public override string CurrentRevision { + get { + + // Remove stale rebase-apply files because it + // makes the method return the wrong hashes. + string rebase_apply_file = SparkleHelpers.CombineMore (LocalPath, ".git", "rebase-apply"); + if (File.Exists (rebase_apply_file)) + File.Delete (rebase_apply_file); + + SparkleGit git = new SparkleGit (LocalPath, "log -1 --format=%H"); + git.Start (); + git.WaitForExit (); + + if (git.ExitCode == 0) { + string output = git.StandardOutput.ReadToEnd (); + return output.TrimEnd (); + } else { + return null; + } + } + } + + + public override bool CheckForRemoteChanges () + { + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Checking for remote changes..."); + SparkleGit git = new SparkleGit (LocalPath, "ls-remote origin master"); + + git.Start (); + git.WaitForExit (); + + if (git.ExitCode != 0) + return false; + + string remote_revision = git.StandardOutput.ReadToEnd ().TrimEnd (); + + if (!remote_revision.StartsWith (CurrentRevision)) { + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Remote changes found. (" + remote_revision + ")"); + return true; + } else { + return false; + } + } + + + public override bool SyncUp () + { + Add (); + + string message = FormatCommitMessage (); + Commit (message); + + SparkleGit git = new SparkleGit (LocalPath, "push origin master"); + + git.Start (); + git.WaitForExit (); + + if (git.ExitCode == 0) + return true; + else + return false; + } + + + public override bool SyncDown () + { + SparkleGit git = new SparkleGit (LocalPath, "fetch -v origin master"); + + git.Start (); + git.WaitForExit (); + + if (git.ExitCode == 0) { + Rebase (); + return true; + } else { + return false; + } + } + + + public override bool AnyDifferences { + get { + SparkleGit git = new SparkleGit (LocalPath, "status --porcelain"); + git.Start (); + git.WaitForExit (); + + string output = git.StandardOutput.ReadToEnd ().TrimEnd (); + string [] lines = output.Split ("\n".ToCharArray ()); + + foreach (string line in lines) { + if (line.Length > 1 && !line [1].Equals (" ")) + return true; + } + + return false; + } + } + + + public override bool HasUnsyncedChanges { + get { + string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath, + ".git", "has_unsynced_changes"); + + return File.Exists (unsynced_file_path); + } + + set { + string unsynced_file_path = SparkleHelpers.CombineMore (LocalPath, + ".git", "has_unsynced_changes"); + + if (value) { + if (!File.Exists (unsynced_file_path)) + File.Create (unsynced_file_path); + } else { + File.Delete (unsynced_file_path); + } + } + } + + + // Stages the made changes + private void Add () + { + SparkleGit git = new SparkleGit (LocalPath, "add --all"); + git.Start (); + git.WaitForExit (); + + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Changes staged"); + } + + + // Removes unneeded objects + private void CollectGarbage () + { + SparkleGit git = new SparkleGit (LocalPath, "gc"); + git.Start (); + git.WaitForExit (); + + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Garbage collected."); + } + + + // Commits the made changes + private void Commit (string message) + { + if (!AnyDifferences) + return; + + SparkleGit git = new SparkleGit (LocalPath, "commit -m '" + message + "'"); + git.Start (); + git.WaitForExit (); + + SparkleHelpers.DebugInfo ("Commit", "[" + Name + "] " + message); + + // Collect garbage pseudo-randomly. Turn off for + // now: too resource heavy. + // if (DateTime.Now.Second % 10 == 0) + // CollectGarbage (); + } + + + // Merges the fetched changes + private void Rebase () + { + DisableWatching (); + + if (AnyDifferences) { + Add (); + + string commit_message = FormatCommitMessage (); + Commit (commit_message); + } + + SparkleGit git = new SparkleGit (LocalPath, "rebase -v FETCH_HEAD"); + + git.Start (); + git.WaitForExit (); + + if (git.ExitCode != 0) { + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Conflict detected. Trying to get out..."); + + while (AnyDifferences) + ResolveConflict (); + + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Conflict resolved."); + + OnConflictResolved (); + } + + EnableWatching (); + } + + + private void ResolveConflict () + { + // This is al list of conflict status codes that Git uses, their + // meaning, and how SparkleShare should handle them. + // + // DD unmerged, both deleted -> Do nothing + // AU unmerged, added by us -> Use theirs, save ours as a timestamped copy + // UD unmerged, deleted by them -> Use ours + // UA unmerged, added by them -> Use theirs, save ours as a timestamped copy + // DU unmerged, deleted by us -> Use theirs + // AA unmerged, both added -> Use theirs, save ours as a timestamped copy + // UU unmerged, both modified -> Use theirs, save ours as a timestamped copy + // + // Note that a rebase merge works by replaying each commit from the working branch on + // top of the upstream branch. Because of this, when a merge conflict happens the + // side reported as 'ours' is the so-far rebased series, starting with upstream, + // and 'theirs' is the working branch. In other words, the sides are swapped. + // + // So: 'ours' means the 'server's version' and 'theirs' means the 'local version' + + SparkleGit git_status = new SparkleGit (LocalPath, "status --porcelain"); + git_status.Start (); + git_status.WaitForExit (); + + string output = git_status.StandardOutput.ReadToEnd ().TrimEnd (); + string [] lines = output.Split ("\n".ToCharArray ()); + + foreach (string line in lines) { + string conflicting_path = line.Substring (3); + conflicting_path = conflicting_path.Trim ("\"".ToCharArray ()); + + SparkleHelpers.DebugInfo ("Git", "[" + Name + "] Conflict type: " + line); + + // Both the local and server version have been modified + if (line.StartsWith ("UU") || line.StartsWith ("AA") || + line.StartsWith ("AU") || line.StartsWith ("UA")) { + + // Recover local version + SparkleGit git_theirs = new SparkleGit (LocalPath, + "checkout --theirs \"" + conflicting_path + "\""); + git_theirs.Start (); + git_theirs.WaitForExit (); + + // Append a timestamp to local version + string timestamp = DateTime.Now.ToString ("HH:mm MMM d"); + string their_path = conflicting_path + " (" + SparkleConfig.DefaultConfig.UserName + ", " + timestamp + ")"; + string abs_conflicting_path = Path.Combine (LocalPath, conflicting_path); + string abs_their_path = Path.Combine (LocalPath, their_path); + + File.Move (abs_conflicting_path, abs_their_path); + + // Recover server version + SparkleGit git_ours = new SparkleGit (LocalPath, + "checkout --ours \"" + conflicting_path + "\""); + git_ours.Start (); + git_ours.WaitForExit (); + + Add (); + + SparkleGit git_rebase_continue = new SparkleGit (LocalPath, "rebase --continue"); + git_rebase_continue.Start (); + git_rebase_continue.WaitForExit (); + } + + // The local version has been modified, but the server version was removed + if (line.StartsWith ("DU")) { + + // The modified local version is already in the + // checkout, so it just needs to be added. + // + // We need to specifically mention the file, so + // we can't reuse the Add () method + SparkleGit git_add = new SparkleGit (LocalPath, + "add " + conflicting_path); + git_add.Start (); + git_add.WaitForExit (); + + SparkleGit git_rebase_continue = new SparkleGit (LocalPath, "rebase --continue"); + git_rebase_continue.Start (); + git_rebase_continue.WaitForExit (); + } + + // The server version has been modified, but the local version was removed + if (line.StartsWith ("UD")) { + + // We can just skip here, the server version is + // already in the checkout + SparkleGit git_rebase_skip = new SparkleGit (LocalPath, "rebase --skip"); + git_rebase_skip.Start (); + git_rebase_skip.WaitForExit (); + } + } + } + + + // Returns a list of the latest change sets + // TODO: Method needs to be made a lot faster + public override List <SparkleChangeSet> GetChangeSets (int count) + { + if (count < 1) + count = 30; + + List <SparkleChangeSet> change_sets = new List <SparkleChangeSet> (); + + SparkleGit git_log = new SparkleGit (LocalPath, "log -" + count + " --raw -M --date=iso"); + Console.OutputEncoding = System.Text.Encoding.Unicode; + git_log.Start (); + + // Reading the standard output HAS to go before + // WaitForExit, or it will hang forever on output > 4096 bytes + string output = git_log.StandardOutput.ReadToEnd (); + git_log.WaitForExit (); + + string [] lines = output.Split ("\n".ToCharArray ()); + List <string> entries = new List <string> (); + + int j = 0; + string entry = "", last_entry = ""; + foreach (string line in lines) { + if (line.StartsWith ("commit") && j > 0) { + entries.Add (entry); + entry = ""; + } + + entry += line + "\n"; + j++; + + last_entry = entry; + } + + entries.Add (last_entry); + + Regex merge_regex = new Regex (@"commit ([a-z0-9]{40})\n" + + "Merge: .+ .+\n" + + "Author: (.+) <(.+)>\n" + + "Date: ([0-9]{4})-([0-9]{2})-([0-9]{2}) " + + "([0-9]{2}):([0-9]{2}):([0-9]{2}) .([0-9]{4})\n" + + "*", RegexOptions.Compiled); + + Regex non_merge_regex = new Regex (@"commit ([a-z0-9]{40})\n" + + "Author: (.+) <(.+)>\n" + + "Date: ([0-9]{4})-([0-9]{2})-([0-9]{2}) " + + "([0-9]{2}):([0-9]{2}):([0-9]{2}) (.[0-9]{4})\n" + + "*", RegexOptions.Compiled); + + // TODO: Need to optimise for speed + foreach (string log_entry in entries) { + Regex regex; + bool is_merge_commit = false; + + if (log_entry.Contains ("\nMerge: ")) { + regex = merge_regex; + is_merge_commit = true; + } else { + regex = non_merge_regex; + } + + Match match = regex.Match (log_entry); + + if (match.Success) { + SparkleChangeSet change_set = new SparkleChangeSet (); + + change_set.Revision = match.Groups [1].Value; + change_set.UserName = match.Groups [2].Value; + change_set.UserEmail = match.Groups [3].Value; + change_set.IsMerge = is_merge_commit; + + + change_set.Timestamp = new DateTime (int.Parse (match.Groups [4].Value), + int.Parse (match.Groups [5].Value), int.Parse (match.Groups [6].Value), + int.Parse (match.Groups [7].Value), int.Parse (match.Groups [8].Value), + int.Parse (match.Groups [9].Value)); + + + string time_zone = match.Groups [10].Value; + int our_offset = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now).Hours; + int their_offset = int.Parse (time_zone.Substring (1, 2)); + + // Convert their timestamp to UTC timezone + if (their_offset > 0) + change_set.Timestamp = change_set.Timestamp.AddHours (their_offset * -1); + else + change_set.Timestamp = change_set.Timestamp.AddHours (their_offset); + + // Convert the UTC timestamp into our timezone + if (our_offset > 0) + change_set.Timestamp = change_set.Timestamp.AddHours (our_offset); + else + change_set.Timestamp = change_set.Timestamp.AddHours (our_offset * -1); + + + string [] entry_lines = log_entry.Split ("\n".ToCharArray ()); + + foreach (string entry_line in entry_lines) { + if (entry_line.StartsWith (":")) { + + string change_type = entry_line [37].ToString (); + string file_path = entry_line.Substring (39); + string to_file_path; + + if (change_type.Equals ("A")) { + change_set.Added.Add (file_path); + } else if (change_type.Equals ("M")) { + change_set.Edited.Add (file_path); + } else if (change_type.Equals ("D")) { + change_set.Deleted.Add (file_path); + } else if (change_type.Equals ("R")) { + int tab_pos = entry_line.LastIndexOf ("\t"); + file_path = entry_line.Substring (42, tab_pos - 42); + to_file_path = entry_line.Substring (tab_pos + 1); + + change_set.MovedFrom.Add (file_path); + change_set.MovedTo.Add (to_file_path); + } + } + } + + change_sets.Add (change_set); + } + } + + return change_sets; + } + + + // Creates a pretty commit message based on what has changed + private string FormatCommitMessage () + { + List<string> Added = new List<string> (); + List<string> Modified = new List<string> (); + List<string> Removed = new List<string> (); + string file_name = ""; + string message = ""; + + SparkleGit git_status = new SparkleGit (LocalPath, "status --porcelain"); + git_status.Start (); + + // Reading the standard output HAS to go before + // WaitForExit, or it will hang forever on output > 4096 bytes + string output = git_status.StandardOutput.ReadToEnd ().Trim ("\n".ToCharArray ()); + git_status.WaitForExit (); + + string [] lines = output.Split ("\n".ToCharArray ()); + foreach (string line in lines) { + if (line.StartsWith ("A")) + Added.Add (line.Substring (3)); + else if (line.StartsWith ("M")) + Modified.Add (line.Substring (3)); + else if (line.StartsWith ("D")) + Removed.Add (line.Substring (3)); + else if (line.StartsWith ("R")) { + Removed.Add (line.Substring (3, (line.IndexOf (" -> ") - 3))); + Added.Add (line.Substring (line.IndexOf (" -> ") + 4)); + } + } + + int count = 0; + int max_count = 20; + + string n = Environment.NewLine; + + foreach (string added in Added) { + file_name = added.Trim ("\"".ToCharArray ()); + message += "+ ‘" + file_name + "’" + n; + + count++; + if (count == max_count) + return message + "..."; + } + + foreach (string modified in Modified) { + file_name = modified.Trim ("\"".ToCharArray ()); + message += "/ ‘" + file_name + "’" + n; + + count++; + if (count == max_count) + return message + "..."; + } + + foreach (string removed in Removed) { + file_name = removed.Trim ("\"".ToCharArray ()); + message += "- ‘" + file_name + "’" + n; + + count++; + if (count == max_count) + return message + "..." + n; + } + + return message.TrimEnd (); + } + + + public override void CreateInitialChangeSet () + { + base.CreateInitialChangeSet (); + Add (); + + string message = FormatCommitMessage (); + Commit (message); + } + + + public override bool UsesNotificationCenter + { + get { + string file_path = SparkleHelpers.CombineMore (LocalPath, ".git", "disable_notification_center"); + return !File.Exists (file_path); + } + } + } +} |