lib/svn-command/subversion.rb in svn-command-0.2.4 vs lib/svn-command/subversion.rb in svn-command-0.2.5

- old
+ new

@@ -8,20 +8,22 @@ gem 'facets', '>=1.8.51' require 'facets/core/kernel/require_local' require 'facets/core/enumerable/uniq_by' require 'facets/core/kernel/silence_stream' +require 'facets/core/module/initializer' gem 'qualitysmith_extensions', '>=0.0.7' # Had a lot of trouble getting ActiveSupport to load without giving errors! Eventually gave up on that idea since I only needed it for mattr_accessor and Facets supplies that. #gem 'activesupport' # mattr_accessor #require 'active_support' #require 'active_support/core_ext/module/attribute_accessors' #require 'facets/core/class/cattr' gem 'qualitysmith_extensions' require 'qualitysmith_extensions/module/attribute_accessors' +require 'qualitysmith_extensions/module/guard_method' # RSCM is used for some of the abstraction, such as for parsing log messages into nice data structures. It seems like overkill, though, to use RSCM for most things... gem 'rscm' #require 'rscm' #require 'rscm/scm/subversion' @@ -30,10 +32,11 @@ # Wraps the Subversion shell commands for Ruby. module Subversion # True if you want output from svn to be colorized (useful if output is for human eyes, but not useful if using the output programatically) @@color = false mattr_accessor :color + mguard_method :with_color!, :@@color # If true, will only output which command _would_ have been executed but will not actually execute it. @@dry_run = false mattr_accessor :dry_run @@ -76,10 +79,14 @@ options[:as] ||= File.basename(repo_url) options[:as] = options[:as].ljust(29) add_to_property 'externals', options[:local_path], "#{options[:as]} #{repo_url}" end + def self.export(path_or_url, target) + execute "export #{path_or_url} #{target}" + end + # Removes the given items from the repository and the disk. Items may contain wildcards. def self.remove(*args) execute "rm #{args.join ' '}" end @@ -141,10 +148,15 @@ def self.update(*args) args = ['./'] if args.empty? execute("update #{args.join ' '}") end + def self.commit(*args) + args = ['./'] if args.empty? + execute("commit #{args.join ' '}") + end + # The output from `svn status` is nicely divided into two "sections": the section which pertains to the current working copy (not # counting externals as part of the working copy) and then the section with status of all of the externals. # This method returns the first section. def self.status_the_section_before_externals(path = './') status = status(path) || '' @@ -178,16 +190,24 @@ }.uniq_by { |external| external.container_dir } end - # Returns the local modifications to the working directory specified by +path+. + # Returns the modifications to the working directory or URL specified in +args+. def self.diff(*args) args = ['./'] if args.empty? - #puts("diff #{"--diff-cmd colordiff" if color} #{args.join ' '}") - execute("diff #{"--diff-cmd colordiff" if color} #{args.join ' '}") + execute("diff #{"--diff-cmd colordiff" if color?} #{args.join ' '}") end + # Parses the output from diff and returns an array of Diff objects. + def self.diffs(*args) + args = ['./'] if args.empty? + raw_diffs = nil + with_color! false do + raw_diffs = diff(*args) + end + DiffsParser.new(raw_diffs).parse + end # It's easy to get/set properties, but less easy to add to a property. This method uses get/set to simulate add. # It will uniquify lines, removing duplicates. (:todo: what if we want to set a property to have some duplicate lines?) def self.add_to_property(property, path, *new_lines) # :todo: I think it's possible to have properties other than svn:* ... so if property contains a prefix (something:), use it; else default to 'svn:' @@ -262,13 +282,20 @@ execute "log #{args.join(' ')}" end # Returns the revision number for head. def self.latest_revision(*args) args = ['./'] if args.empty? + # The revision returned by svn status -u seems to be a pretty reliable way to get this. Does anyone know of a better way? matches = /Status against revision:\s+(\d+)/m.match(status_against_server(args)) matches && matches[1] end + # Returns the revision number for the working directory(/file?) specified by +path+ + def self.latest_revision_for_path(path) + # The revision returned by svn info seems to be a pretty reliable way to get this. Does anyone know of a better way? + matches = info(path).match(/^Revision: (\d+)/) + matches && matches[1] + end # Returns an array of RSCM::Revision objects def self.revisions(*args) # Tried using this, but it seems to expect you to pass in a starting date or accept the default starting date of right now, which is silly if you actually just want *all* revisions... #@rscm = ::RSCM::Subversion.new @@ -285,14 +312,19 @@ def self.info(*args) args = ['./'] if args.empty? execute "info #{args.join(' ')}" end + def self.url(path_or_url = './') + matches = info(path_or_url).match(/^URL: (.+)/) + matches && matches[1] + end + # :todo: needs some serious unit-testing love def self.base_url(path_or_url = './') - matches = info(path_or_url).match(/^Repository Root: (.+)/) - matches && matches[1] + matches = info(path_or_url).match(/^Repository Root: (.+)/) + matches && matches[1] # It appears that we might need to use this old way (which looks at 'URL'), since there is actually a # base_url = nil # needed so that base_url variable isn't local to loop block (and reset during next iteration)! # started_using_dot_dots = false # loop do @@ -405,14 +437,12 @@ -#Subversion.const_set(:RevisionProperty) = Struct.new(:name, :repository_path) - +#----------------------------------------------------------------------------------------------------------------------------- module Subversion - RevisionProperty = Struct.new(:name, :value) # Represents an "externals container", which is a directory that has the <tt>svn:externals</tt> property set to something useful. # Each ExternalsContainer contains a set of "entries", which are the actual directories listed in the <tt>svn:externals</tt> # property and are "pulled into" the directory. @@ -456,8 +486,64 @@ def ==(other) self.container_dir == other.container_dir end end -end + # A collection of Diff objects in in file_name => diff format. + class Diffs < Hash + end + + class Diff + attr_reader :filename, :diff + initializer :filename do + @diff = '' + end + def filename_pretty + filename.ljust(100).black_on_white + end + end + + class DiffsParser + class ParseError < Exception; end + initializer :raw_diffs + @state = nil + def parse + diffs = Diffs.new + current_diff = nil + @raw_diffs.each_line do |line| + if line =~ /^Index: (.*)$/ + current_diff = Diff.new($1) + diffs[current_diff.filename] = current_diff #unless current_diff.nil? + @state = :immediately_after_filename + next + end + + if current_diff.nil? + raise ParseError.new("The raw diff input didn't begin with 'Index:'!") + end + + if @state == :immediately_after_filename + if line =~ /^===================================================================$/ || + line =~ /^---.*\(revision \d+\)$/ || + line =~ /^\+\+\+.*\(revision \d+\)$/ || + line =~ /^@@ .* @@$/ + # Skip + next + else + @state= :inside_the_actual_diff + end + end + + if @state == :inside_the_actual_diff + current_diff.diff << line + else + raise ParseError.new("Expected to be in :inside_the_actual_diff state, but was not.") + end + end + diffs.freeze + diffs + end + end + +end end