diff --git a/Makefile.am b/Makefile.am index a55ddf4d..4e58475b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2,6 +2,7 @@ SUBDIRS = \ build \ notify-sharp \ SparkleShare \ + SparkleDiff \ data \ po diff --git a/README b/README index c8f0549b..00e144af 100644 --- a/README +++ b/README @@ -34,10 +34,10 @@ SparkleShare currently requires: - gtk-sharp2 >= 2.12.7 - mono-core >= 2.2 - ndesk-dbus >= 0.6 - - notify-sharp >= 0.4.0 - openssh - gvfs >= 1.3 - intltool + - nautilus-python Run the service: @@ -63,7 +63,6 @@ To build SparkleShare you need: - monodevelop >= 2.0 - ndesk-dbus-devel >= 0.6 - ndesk-dbus-glib-devel >= 0.6 - - notify-sharp-devel >= 0.4.0 You can build and install SparkleShare like this: diff --git a/SparkleDiff/Makefile.am b/SparkleDiff/Makefile.am new file mode 100644 index 00000000..0f90c96b --- /dev/null +++ b/SparkleDiff/Makefile.am @@ -0,0 +1,10 @@ +ASSEMBLY = SparkleDiff +TARGET = exe + +LINK = $(REF_SPARKLEDIFF) + +SOURCES = \ +$(top_srcdir)/SparkleShare/Defines.cs \ +SparkleDiff.cs + +include $(top_srcdir)/build/build.mk diff --git a/SparkleDiff/SparkleDiff.cs b/SparkleDiff/SparkleDiff.cs new file mode 100644 index 00000000..878ee038 --- /dev/null +++ b/SparkleDiff/SparkleDiff.cs @@ -0,0 +1,418 @@ +// SparkleShare, an instant update workflow to Git. +// Copyright (C) 2010 Hylke Bons +// +// 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 . + +using Gtk; +using Mono.Unix; +using System; +using System.Diagnostics; +using System.IO; +using System.Text.RegularExpressions; + +namespace SparkleShare { + + public class SparkleDiff + { + + public static void Main (string [] args) + { + Catalog.Init (Defines.GETTEXT_PACKAGE, Defines.LOCALE_DIR); + + if (args.Length > 0) { + + string file_path = args [0]; + + if (File.Exists (file_path)) { + + Gtk.Application.Init (); + + SparkleDiffWindow sparkle_diff_window; + sparkle_diff_window = new SparkleDiffWindow (file_path); + sparkle_diff_window.ShowAll (); + + // The main loop + Gtk.Application.Run (); + + } else { + + Console.WriteLine ("SparkleDiff: " + file_path + ": No such file or directory."); + Environment.Exit (0); + + } + + } + + } + + } + + + public class SparkleDiffWindow : Window + { + + // Short alias for the translations + public static string _ (string s) + { + return Catalog.GetString (s); + } + + private RevisionView ViewLeft; + private RevisionView ViewRight; + + private string [] Revisions; + + public SparkleDiffWindow (string file_path) : base ("") + { + + string file_name = System.IO.Path.GetFileName (file_path); + + SetSizeRequest (800, 540); + SetPosition (WindowPosition.Center); + + BorderWidth = 12; + + DeleteEvent += Quit; + + IconName = "image-x-generic"; + // TRANSLATORS: The parameter is a filename + Title = String.Format(_("Comparing Revisions of ‘{0}’"), file_name); + + Revisions = GetRevisionsForFile (file_path); + + VBox layout_vertical = new VBox (false, 12); + + HBox layout_horizontal = new HBox (false, 12); + + Process process = new Process (); + process.EnableRaisingEvents = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.UseShellExecute = false; + + // TODO: Nice commit summary and "Current Revision" + process.StartInfo.WorkingDirectory = System.IO.Path.GetDirectoryName (file_path); + process.StartInfo.FileName = "git"; + process.StartInfo.Arguments = "log --format=\"%ct\t%an\" " + file_name; + process.Start (); + + string output = process.StandardOutput.ReadToEnd (); + + string [] revisions_info = Regex.Split (output.Trim (), "\n"); + + int i = 0; + foreach (string revision_info in revisions_info) { + + string [] parts = Regex.Split (revision_info.Trim (), "\t"); + + int timestamp = int.Parse (parts [0]); + string author = parts [1]; + + if (i == 0) + revisions_info [i] = _("Current Revision") + "\t" + author; + else + // TRANSLATORS: This is a format specifier according to System.Globalization.DateTimeFormatInfo + revisions_info [i] = UnixTimestampToDateTime (timestamp).ToString (_("d MMM\tH:mm")) + + "\t" + author; + + i++; + + } + + ViewLeft = new RevisionView (revisions_info); + ViewRight = new RevisionView (revisions_info); + + ViewLeft.ComboBox.Active = 1; + ViewRight.ComboBox.Active = 0; + + RevisionImage revision_image_left = new RevisionImage (file_path, Revisions [1]); + RevisionImage revision_image_right = new RevisionImage (file_path, Revisions [0]); + + ViewLeft.SetImage (revision_image_left); + ViewRight.SetImage (revision_image_right); + + ViewLeft.ComboBox.Changed += delegate { + + RevisionImage revision_image; + revision_image = new RevisionImage (file_path, Revisions [ViewLeft.ComboBox.Active]); + ViewLeft.SetImage (revision_image); + + HookUpViews (); + + ViewLeft.ScrolledWindow.Hadjustment = ViewRight.ScrolledWindow.Hadjustment; + ViewLeft.ScrolledWindow.Vadjustment = ViewRight.ScrolledWindow.Vadjustment; + + ViewLeft.UpdateControls (); + + }; + + ViewRight.ComboBox.Changed += delegate { + + RevisionImage revision_image; + revision_image = new RevisionImage (file_path, Revisions [ViewRight.ComboBox.Active]); + ViewRight.SetImage (revision_image); + + HookUpViews (); + + ViewRight.ScrolledWindow.Hadjustment = ViewLeft.ScrolledWindow.Hadjustment; + ViewRight.ScrolledWindow.Vadjustment = ViewLeft.ScrolledWindow.Vadjustment; + + ViewRight.UpdateControls (); + + }; + + layout_horizontal.PackStart (ViewLeft); + layout_horizontal.PackStart (ViewRight); + + HookUpViews (); + + HButtonBox dialog_buttons = new HButtonBox (); + dialog_buttons.Layout = ButtonBoxStyle.End; + dialog_buttons.BorderWidth = 0; + + Button CloseButton = new Button (Stock.Close); + CloseButton.Clicked += delegate (object o, EventArgs args) { + Environment.Exit (0); + }; + + dialog_buttons.Add (CloseButton); + + layout_vertical.PackStart (layout_horizontal, true, true, 0); + layout_vertical.PackStart (dialog_buttons, false, false, 0); + + Add (layout_vertical); + + } + + + // Hooks up two views so they will be kept in sync + private void HookUpViews () { + + ViewLeft.ScrolledWindow.Hadjustment.ValueChanged += SyncViewsHorizontally; + ViewLeft.ScrolledWindow.Vadjustment.ValueChanged += SyncViewsVertically; + ViewRight.ScrolledWindow.Hadjustment.ValueChanged += SyncViewsHorizontally; + ViewRight.ScrolledWindow.Vadjustment.ValueChanged += SyncViewsVertically; + + } + + + // Keeps the two image views in sync horizontally + private void SyncViewsHorizontally (object o, EventArgs args) { + + Adjustment source_adjustment = (Adjustment) o; + + if (source_adjustment == ViewLeft.ScrolledWindow.Hadjustment) + ViewRight.ScrolledWindow.Hadjustment = source_adjustment; + else + ViewLeft.ScrolledWindow.Hadjustment = source_adjustment; + + } + + + // Keeps the two image views in sync vertically + private void SyncViewsVertically (object o, EventArgs args) { + + Adjustment source_adjustment = (Adjustment) o; + + if (source_adjustment == ViewLeft.ScrolledWindow.Vadjustment) + ViewRight.ScrolledWindow.Vadjustment = source_adjustment; + else + ViewLeft.ScrolledWindow.Vadjustment = source_adjustment; + + } + + + // Gets a list of all earlier revisions of this file + private string [] GetRevisionsForFile (string file_path) + { + + string file_name = System.IO.Path.GetFileName (file_path); + + Process process = new Process (); + process.EnableRaisingEvents = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.UseShellExecute = false; + + // TODO: Nice commit summary and "Current Revision" + process.StartInfo.WorkingDirectory = System.IO.Path.GetDirectoryName (file_path); + process.StartInfo.FileName = "git"; + process.StartInfo.Arguments = "log --format=\"%H\" " + file_name; + process.Start (); + + string output = process.StandardOutput.ReadToEnd (); + + return Regex.Split (output.Trim (), "\n"); + + } + + + // Converts a UNIX timestamp to a more usable time object + public DateTime UnixTimestampToDateTime (int timestamp) + { + DateTime unix_epoch = new DateTime (1970, 1, 1, 0, 0, 0, 0); + return unix_epoch.AddSeconds (timestamp); + } + + + // Quits the program + private void Quit (object o, EventArgs args) { + + Environment.Exit (0); + + } + + + } + + + // An image grabbed from a stream generated by Git + public class RevisionImage : Image + { + + public string Revision; + public string FilePath; + + public RevisionImage (string file_path, string revision) : base () + { + + Revision = revision; + FilePath = file_path; + + Process process = new Process (); + process.EnableRaisingEvents = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.UseShellExecute = false; + + process.StartInfo.WorkingDirectory = System.IO.Path.GetDirectoryName (FilePath); + process.StartInfo.FileName = "git"; + process.StartInfo.Arguments = "show " + revision + ":" + System.IO.Path.GetFileName (FilePath); + process.Start (); + + Pixbuf = new Gdk.Pixbuf ((System.IO.Stream) process.StandardOutput.BaseStream); + + } + + } + + + // A custom widget containing an image view, + // previous/next buttons and a combobox + public class RevisionView : VBox + { + + public ScrolledWindow ScrolledWindow; + public ComboBox ComboBox; + public Button ButtonPrevious; + public Button ButtonNext; + + private int ValueCount; + private Image Image; + + public RevisionView (string [] revisions) : base (false, 6) + { + + Image = new Image (); + + ScrolledWindow = new ScrolledWindow (); + ScrolledWindow.AddWithViewport (Image); + PackStart (ScrolledWindow, true, true, 0); + + HBox controls = new HBox (false, 6); + controls.BorderWidth = 0; + + Image image_previous = new Image (); + image_previous.IconName = "go-previous"; + ButtonPrevious = new Button (image_previous); + ButtonPrevious.Clicked += PreviousInComboBox; + + ValueCount = 0; + + ComboBox = ComboBox.NewText (); + + foreach (string revision in revisions) { + ComboBox.AppendText (revision); + } + + ComboBox.Active = 0; + + ValueCount = revisions.Length; + + Image image_next = new Image (); + image_next.IconName = "go-next"; + ButtonNext = new Button (image_next); + ButtonNext.Clicked += NextInComboBox; + +// controls.PackStart (ButtonPrevious, false, false, 0); + controls.PackStart (ComboBox, false, false, 0); +// controls.PackStart (ButtonNext, false, false, 0); + + PackStart (controls, false, false, 0); + + UpdateControls (); + + } + + + public void NextInComboBox (object o, EventArgs args) { + +/* if (ComboBox.Active > 0) + ComboBox.Active--; + + UpdateControls (); +*/ + } + + + public void PreviousInComboBox (object o, EventArgs args) { + +/* if (ComboBox.Active + 1 < ValueCount) + ComboBox.Active++; + + UpdateControls (); +*/ + } + + + // Changes the image that is viewed + public void SetImage (Image image) { + + Image = image; + Remove (ScrolledWindow); + ScrolledWindow = new ScrolledWindow (); + ScrolledWindow.AddWithViewport (Image); + Add (ScrolledWindow); + ReorderChild (ScrolledWindow, 0); + ShowAll (); + + } + + + // Updates the buttons to be disabled or enabled when needed + public void UpdateControls () { + + // TODO: Doesn't work yet. Sleepy -.- +/* ButtonPrevious.State = StateType.Normal; + ButtonNext.State = StateType.Normal; + + if (ComboBox.Active == 0) + ButtonNext.State = StateType.Insensitive; + + if (ComboBox.Active + 1 == ValueCount) + ButtonPrevious.State = StateType.Insensitive; +*/ + + } + + } + +} diff --git a/SparkleShare/Makefile.am b/SparkleShare/Makefile.am index 2165fbd7..29ea226f 100644 --- a/SparkleShare/Makefile.am +++ b/SparkleShare/Makefile.am @@ -1,3 +1,6 @@ +SUBDIRS = \ + Nautilus + ASSEMBLY = SparkleShare TARGET = exe diff --git a/SparkleShare/Nautilus/Makefile.am b/SparkleShare/Nautilus/Makefile.am new file mode 100644 index 00000000..a821d5e6 --- /dev/null +++ b/SparkleShare/Nautilus/Makefile.am @@ -0,0 +1,10 @@ +SOURCES = \ + sparkleshare-nautilus-extension.py + +if NAUTILUS_EXTENSION_ENABLED +NAUTILUS_PYTHON_INSTALL_DIR=$(subst $(NAUTILUS_LIBDIR),${libdir},$(NAUTILUS_PYTHON_DIR)) +extensiondir = $(NAUTILUS_PYTHON_INSTALL_DIR) +extension_SCRIPTS = $(addprefix $(srcdir)/, $(SOURCES)) +else +EXTRA_DIST = $(SOURCES) +endif diff --git a/SparkleShare/Nautilus/sparkleshare-nautilus-extension.py b/SparkleShare/Nautilus/sparkleshare-nautilus-extension.py new file mode 100644 index 00000000..56885fb1 --- /dev/null +++ b/SparkleShare/Nautilus/sparkleshare-nautilus-extension.py @@ -0,0 +1,132 @@ +# SparkleShare, an instant update workflow to Git. +# Copyright (C) 2010 Hylke Bons +# +# 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 . + +import os +import shutil +import time + +import gio +import nautilus + +SPARKLESHARE_PATH = os.path.join (os.path.expanduser ('~'), "SparkleShare") + +class SparkleShareExtension (nautilus.MenuProvider): + + + def __init__ (self): + + debug = "Loaded Nautilus SparkleShare Extension." + + + def checkout_version (self, menu, file_reference, commit_hash, username, timestamp): + + file_name = file_reference.get_basename ().replace (" ", "\ ").replace ("(", "\(").replace (")", "\)") + file_path = file_reference.get_path ().replace (" ", "\ ").replace ("(", "\(").replace (")", "\)") + tmp_file_path = os.path.join (SPARKLESHARE_PATH, ".tmp", file_reference.get_basename ()) + + # Move the current version to a temporary path + shutil.move (file_reference.get_path (), tmp_file_path) + + # Check out the earlier version + os.chdir (file_reference.get_parent ().get_path ()) + os.popen ("git checkout " + commit_hash + " " + file_name + .replace (" ", "\ ").replace ("(", "\(").replace (")", "\)")) + + new_tmp_file_name = file_name + " (" + username + ", " + new_tmp_file_name += time.strftime ("%H:%M %d %b %Y", timestamp).replace (" 0", " ") + ") " + + # Rename the checked out file + shutil.move (file_name, new_tmp_file_name) + + # Move the original file back + shutil.move (tmp_file_path, file_path) + + return True + + + def compare_versions (self, menu, file_reference): + return + + + def get_file_items (self, window, files): + + # Only work if one file is selected + if len (files) != 1: + return + + file_reference = gio.File (files [0].get_uri ()) + + # Only work if we're in a SparkleShare repository folder + if not (file_reference.get_path ().startswith (SPARKLESHARE_PATH)): + return + + epochs = ["", "", "", "", "", "", "", "", "", ""] + commit_hashes = ["", "", "", "", "", "", "", "", "", ""] + + os.chdir (file_reference.get_parent ().get_path ()) + + time_command = os.popen ("git log -10 --format='%at' " + file_reference.get_path () + .replace (" ", "\ ").replace ("(", "\(").replace (")", "\)")) + + author_command = os.popen ("git log -10 --format='%an' " + file_reference.get_path () + .replace (" ", "\ ").replace ("(", "\(").replace (")", "\)")) + + hash_command = os.popen ("git log -10 --format='%H' " + file_reference.get_path () + .replace (" ", "\ ").replace ("(", "\(").replace (")", "\)")) + + i = 0 + for line in time_command.readlines (): + epochs [i] = line.strip ("\n") + i += 1 + + if i < 2: + return + + i = 0 + for line in hash_command.readlines (): + commit_hashes [i] = line.strip ("\n") + i += 1 + + earlier_version_menu_item = nautilus.MenuItem ("Nautilus::OpenOlderVersion", "Get Earlier Version", + "Make a copy of an earlier version in this folder") + submenu = nautilus.Menu () + + i = 0 + for line in author_command.readlines (): + + if i > 0: + + timestamp = time.strftime ("%d %b\t%H:%M", time.localtime (float (epochs [i]))) + username = line.strip ("\n") + + menu_item = nautilus.MenuItem ("Nautilus::Version" + epochs [i], + timestamp + "\t" + username, + "Select to get a copy of this version") + + menu_item.connect ("activate", self.checkout_version, file_reference, commit_hashes [i], + username, time.localtime (float (epochs [i]))) + submenu.append_item (menu_item) + + i += 1 + + earlier_version_menu_item.set_submenu (submenu) + +# compare_versions_menu_item = nautilus.MenuItem ("Nautilus::CompareVersions", "Compare Versions", +# "Compare two versions of this document at any point in time") + +# compare_versions_menu_item = menu_item.connect ("activate", self.compare_versions, file_reference) + + return earlier_version_menu_item, diff --git a/SparkleShare/SparkleRepo.cs b/SparkleShare/SparkleRepo.cs index d9dac422..8d856d6a 100644 --- a/SparkleShare/SparkleRepo.cs +++ b/SparkleShare/SparkleRepo.cs @@ -257,10 +257,10 @@ namespace SparkleShare { Process.Start (); DateTime DateTime = new DateTime (); - string TimeStamp = DateTime.Now.ToString ("H:mm, d MMM yyyy"); + string TimeStamp = DateTime.Now.ToString ("H:mm d MMM yyyy"); File.Move (ProblemFileName, - ProblemFileName + " (" + UserName + " - " + TimeStamp + ")"); + ProblemFileName + " (" + UserName + ", " + TimeStamp + ")"); Process.StartInfo.Arguments = "checkout --theirs " + ProblemFileName; diff --git a/build/build.environment.mk b/build/build.environment.mk index 0c8fe3d8..b3c75395 100644 --- a/build/build.environment.mk +++ b/build/build.environment.mk @@ -34,6 +34,10 @@ REF_SPARKLESHARE = $(LINK_SYSTEM) $(LINK_GTK) $(LINK_DBUS) $(LINK_NOTIFY_SHARP_D LINK_SPARKLESHARE = -r:$(DIR_BIN)/SparkleShare.exe LINK_SPARKLESHARE_DEPS = $(REF_SPARKLESHARE) $(LINK_SPARKLESHARE) +REF_SPARKLEDIFF = $(LINK_SYSTEM) $(LINK_GTK) $(LINK_DBUS) $(LINK_MONO_POSIX) +LINK_SPARKLEDIFF = -r:$(DIR_BIN)/SparkleShare.exe +LINK_SPARKLEDIFF_DEPS = $(REF_SPARKLEDIFF) $(LINK_SPARKLEDIFF) + # Cute hack to replace a space with something colon:= : empty:= diff --git a/build/m4/sparkleshare/nautilus-python.m4 b/build/m4/sparkleshare/nautilus-python.m4 new file mode 100644 index 00000000..d7765330 --- /dev/null +++ b/build/m4/sparkleshare/nautilus-python.m4 @@ -0,0 +1,14 @@ +AC_DEFUN([SPARKLESHARE_NAUTILUS_PYTHON], +[ + PKG_CHECK_MODULES(NAUTILUS_PYTHON, nautilus-python, have_nautilus_python=yes, have_nautilus_python=no) + if test "x$have_nautilus_python" = "xyes"; then + NAUTILUS_LIBDIR="`$PKG_CONFIG --variable=libdir nautilus-python`" + AC_SUBST(NAUTILUS_LIBDIR) + NAUTILUS_PYTHON_DIR="`$PKG_CONFIG --variable=pythondir nautilus-python`" + AC_SUBST(NAUTILUS_PYTHON_DIR) + AM_CONDITIONAL(NAUTILUS_EXTENSION_ENABLED, true) + else + AM_CONDITIONAL(NAUTILUS_EXTENSION_ENABLED, false) + fi +]) + diff --git a/configure.ac b/configure.ac index d3333527..02d5bed3 100644 --- a/configure.ac +++ b/configure.ac @@ -75,6 +75,9 @@ SHAMROCK_CHECK_MONO_2_0_GAC_ASSEMBLIES([ Mono.Posix ]) +dnl Get nautilus extensions directory +SPARKLESHARE_NAUTILUS_PYTHON + SHAVE_INIT([build/m4/shave], [enable]) @@ -86,10 +89,13 @@ build/m4/shave/shave-libtool data/Makefile data/icons/Makefile notify-sharp/Makefile +SparkleDiff/Makefile +SparkleDiff/Defines.cs SparkleShare/sparkleshare SparkleShare/Defines.cs SparkleShare/AssemblyInfo.cs SparkleShare/Makefile +SparkleShare/Nautilus/Makefile po/Makefile.in Makefile ]) diff --git a/po/POTFILES.in b/po/POTFILES.in index ad84be9b..0a11dd8e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -2,6 +2,7 @@ # Please keep this file in alphabetical order; run ./sort-potfiles # after adding files here. [encoding: UTF-8] +SparkleDiff/SparkleDiff.cs SparkleShare/SparkleBubble.cs SparkleShare/SparkleDialog.cs SparkleShare/SparkleHelpers.cs