lib/rscm/base.rb in rscm-0.4.5 vs lib/rscm/base.rb in rscm-0.5.0

- old
+ new

@@ -1,281 +1,289 @@ -require 'fileutils' -require 'rscm/revision' -require 'rscm/path_converter' - -module RSCM - # This class defines the RSCM API, which offers access to an SCM working copy - # as well as a 'central' repository. - # - # Concrete subclasses of this class (concrete adapters) implement the integration - # with the respective SCMs. - # - # Most of the methods take an optional +options+ Hash (named parameters), allowing - # the following options: - # - # * <tt>:stdout</tt>: Path to file name where stdout of SCM operations are written. - # * <tt>:stdout</tt>: Path to file name where stderr of SCM operations are written. - # - # In stead of specifying the +options+ parameters for every API method, it's possible - # to assign default options via the +default_options+ attribute. - # - # Some of the methods in this API use +from_identifier+ and +to_identifier+. - # These identifiers can be either a UTC Time (according to the SCM's clock) - # or a String or Integer representing a label/revision - # (according to the SCM's native label/revision scheme). - # - # If +from_identifier+ or +to_identifier+ are +nil+ they should respectively default to - # Time.epoch or Time.infinite. - # - class Base - - attr_writer :default_options - def default_options - @default_options ||= {:stdout=>'stdout.log', :stderr=>'stderr.log'} - end - - # Transforms +raw_identifier+ into the native rype used for revisions. - def to_identifier(raw_identifier) - raw_identifier.to_s - end - - # Sets the checkout dir (working copy). Should be set prior to most other method - # invocations (depending on the implementation). - def checkout_dir=(dir) - @checkout_dir = PathConverter.filepath_to_nativepath(dir, false) - end - - # Gets the working copy directory. - def checkout_dir - @checkout_dir - end - - def to_yaml_properties #:nodoc: - props = instance_variables - props.delete("@checkout_dir") - props.delete("@default_options") - props.sort! - end - - # Destroys the working copy - def destroy_working_copy - FileUtils.rm_rf(checkout_dir) unless checkout_dir.nil? - end - - # Whether or not the SCM represented by this instance exists. - def central_exists? - # The default implementation assumes yes - override if it can be - # determined programmatically. - true - end - - # Whether or not this SCM is transactional (atomic). - def transactional? - false - end - alias :atomic? :transactional? - - # Creates a new 'central' repository. This is intended only for creation of 'central' - # repositories (not for working copies). You shouldn't have to call this method if a central repository - # already exists. This method is used primarily for testing of RSCM, but can also - # be used if you *really* want to use RSCM to create a central repository. - # - # This method should throw an exception if the repository cannot be created (for - # example if the repository is 'remote' or if it already exists). - # - def create_central(options={}) - raise NotImplementedError - end - - # Destroys the central repository. Shuts down any server processes and deletes the repository. - # WARNING: calling this may result in loss of data. Only call this if you really want to wipe - # it out for good! - def destroy_central - raise NotImplementedError - end - - # Whether a repository can be created. - def can_create_central? - false - end - - # Adds +relative_filename+ to the working copy. - def add(relative_filename, options={}) - raise NotImplementedError - end - - # Schedules a move of +relative_src+ to +relative_dest+ - # Should not take effect in the central repository until - # +commit+ is invoked. - def move(relative_src, relative_dest, options={}) - raise NotImplementedError - end - - # Recursively imports files from <tt>:dir</tt> into the central scm, - # using commit message <tt>:message</tt> - def import_central(options) - raise NotImplementedError - end - - # Open a file for edit - required by scms that check out files in read-only mode e.g. perforce - def edit(file, options={}) - end - - # Commit (check in) modified files. - def commit(message, options={}) - raise NotImplementedError - end - - # Checks out or updates contents from a central SCM to +checkout_dir+ - a local working copy. - # If this is a distributed SCM, this method should create a 'working copy' repository - # if one doesn't already exist. Then the contents of the central SCM should be pulled into - # the working copy. - # - # The +to_identifier+ parameter may be optionally specified to obtain files up to a - # particular time or label. +to_identifier+ should either be a Time (in UTC - according to - # the clock on the SCM machine) or a String - reprsenting a label or revision. - # - # This method will yield the relative file name of each checked out file, and also return - # them in an array. Only files, not directories, should be yielded/returned. - # - # This method should be overridden for SCMs that are able to yield checkouts as they happen. - # For some SCMs this is not possible, or at least very hard. In that case, just override - # the checkout_silent method instead of this method (should be protected). - # - def checkout(to_identifier=Time.infinity, options={}) # :yield: file - to_identifier = Time.infinity if to_identifier.nil? - - before = checked_out_files - # We expect subclasses to implement this as a protected method (unless this whole method is overridden). - checkout_silent(to_identifier, options) - after = checked_out_files - - (after - before).sort! - end - - def checked_out_files - files = Dir["#{@checkout_dir}/**/*"] - files.delete_if{|file| File.directory?(file)} - ignore_paths.each do |regex| - files.delete_if{|file| file =~ regex} - end - dir = File.expand_path(@checkout_dir) - files.collect{|file| File.expand_path(file)[dir.length+1..-1]} - end - - # Returns a Revisions object for the interval specified by +from_identifier+ (exclusive, i.e. after) - # and optionally +:to_identifier+ (inclusive). If +relative_path+ is specified, the result will only contain - # revisions pertaining to that path. - # - def revisions(from_identifier, options={}) - raise NotImplementedError - end - - # Returns the HistoricFile representing the root of the repo - def rootdir - file("", true) - end - - # Returns a HistoricFile for +relative_path+ - def file(relative_path, dir) - HistoricFile.new(relative_path, dir, self) - end - - # Opens a revision_file - def open(revision_file, &block) #:yield: io - raise NotImplementedError - end - - # Whether the working copy is in synch with the central - # repository's revision/time identified by +identifier+. - # If +identifier+ is nil, 'HEAD' of repository should be assumed. - # - def uptodate?(identifier) - raise NotImplementedError - end - - # Whether the project is checked out from the central repository or not. - # Subclasses should override this to check for SCM-specific administrative - # files if appliccable - def checked_out? - File.exists?(@checkout_dir) - end - - # Whether triggers are supported by this SCM. A trigger is a command that can be executed - # upon a completed commit to the SCM. - def supports_trigger? - # The default implementation assumes no - override if it can be - # determined programmatically. - false - end - alias :can_install_trigger? :supports_trigger? - - # Descriptive name of the trigger mechanism - def trigger_mechanism - raise NotImplementedError - end - - # Installs +trigger_command+ in the SCM. - # The +install_dir+ parameter should be an empty local - # directory that the SCM can use for temporary files - # if necessary (CVS needs this to check out its administrative files). - # Most implementations will ignore this parameter. - # - def install_trigger(trigger_command, install_dir) - raise NotImplementedError - end - - # Uninstalls +trigger_command+ from the SCM. - # - def uninstall_trigger(trigger_command, install_dir) - raise NotImplementedError - end - - # Whether the command denoted by +trigger_command+ is installed in the SCM. - # - def trigger_installed?(trigger_command, install_dir) - raise NotImplementedError - end - - # The command line to run in order to check out a fresh working copy. - # - def checkout_commandline(to_identifier=Time.infinity) - raise NotImplementedError - end - - # The command line to run in order to update a working copy. - # - def update_commandline(to_identifier=Time.infinity) - raise NotImplementedError - end - - # Returns/yields an IO containing the unified diff of the change. - # Also see RevisionFile#diff - def diff(change, &block) - raise NotImplementedError - end - - def ==(other_scm) - return false if self.class != other_scm.class - self.instance_variables.each do |var| - return false if self.instance_eval(var) != other_scm.instance_eval(var) - end - true - end - - protected - - # Wrapper for CommandLine.execute that provides default values for - # dir plus any options set in default_options (typically stdout and stderr). - def execute(cmd, options={}, &proc) - default_dir = @checkout_dir.nil? ? Dir.pwd : @checkout_dir - options = {:dir => default_dir}.merge(default_options).merge(options) - begin - CommandLine.execute(cmd, options, &proc) - rescue CommandLine::OptionError => e - e.message += "\nEither specify default_options on the scm object, or pass the required options to the method" - raise e - end - end - - end -end +require 'fileutils' +require 'rscm/revision' +require 'rscm/path_converter' + +module RSCM + # This class defines the RSCM API, which offers access to an SCM working copy + # as well as a 'central' repository. + # + # Concrete subclasses of this class (concrete adapters) implement the integration + # with the respective SCMs. + # + # Most of the methods take an optional +options+ Hash (named parameters), allowing + # the following options: + # + # * <tt>:stdout</tt>: Path to file name where stdout of SCM operations are written. + # * <tt>:stdout</tt>: Path to file name where stderr of SCM operations are written. + # + # In stead of specifying the +options+ parameters for every API method, it's possible + # to assign default options via the +default_options+ attribute. + # + # Some of the methods in this API use +from_identifier+ and +to_identifier+. + # These identifiers can be either a UTC Time (according to the SCM's clock) + # or a String or Integer representing a label/revision + # (according to the SCM's native label/revision scheme). + # + # If +from_identifier+ or +to_identifier+ are +nil+ they should respectively default to + # Time.epoch or Time.infinite. + # + class Base + include RevisionPoller + + attr_writer :default_options + attr_writer :store_revisions_command + + def default_options + @default_options ||= {} + end + + # Returns true if the underlying SCM tool is available on this system. + def available? + raise NotImplementedError + end + + # Transforms +raw_identifier+ into the native rype used for revisions. + def to_identifier(raw_identifier) + raw_identifier.to_s + end + + # Sets the checkout dir (working copy). Should be set prior to most other method + # invocations (depending on the implementation). + def checkout_dir=(dir) + @checkout_dir = PathConverter.filepath_to_nativepath(dir, false) + end + + # Gets the working copy directory. + def checkout_dir + @checkout_dir + end + + def to_yaml_properties #:nodoc: + props = instance_variables + props.delete("@checkout_dir") + props.delete("@default_options") + props.sort! + end + + # Destroys the working copy + def destroy_working_copy(options={}) + FileUtils.rm_rf(checkout_dir) unless checkout_dir.nil? + end + + # Whether or not the SCM represented by this instance exists. + def central_exists? + # The default implementation assumes yes - override if it can be + # determined programmatically. + true + end + + # Whether or not this SCM is transactional (atomic). + def transactional? + false + end + alias :atomic? :transactional? + + # Creates a new 'central' repository. This is intended only for creation of 'central' + # repositories (not for working copies). You shouldn't have to call this method if a central repository + # already exists. This method is used primarily for testing of RSCM, but can also + # be used if you *really* want to use RSCM to create a central repository. + # + # This method should throw an exception if the repository cannot be created (for + # example if the repository is 'remote' or if it already exists). + # + def create_central(options={}) + raise NotImplementedError + end + + # Destroys the central repository. Shuts down any server processes and deletes the repository. + # WARNING: calling this may result in loss of data. Only call this if you really want to wipe + # it out for good! + def destroy_central + raise NotImplementedError + end + + # Whether a repository can be created. + def can_create_central? + false + end + + # Adds +relative_filename+ to the working copy. + def add(relative_filename, options={}) + raise NotImplementedError + end + + # Schedules a move of +relative_src+ to +relative_dest+ + # Should not take effect in the central repository until + # +commit+ is invoked. + def move(relative_src, relative_dest, options={}) + raise NotImplementedError + end + + # Recursively imports files from <tt>:dir</tt> into the central scm, + # using commit message <tt>:message</tt> + def import_central(options) + raise NotImplementedError + end + + # Open a file for edit - required by scms that check out files in read-only mode e.g. perforce + def edit(file, options={}) + end + + # Commit (check in) modified files. + def commit(message, options={}) + raise NotImplementedError + end + + # Checks out or updates contents from a central SCM to +checkout_dir+ - a local working copy. + # If this is a distributed SCM, this method should create a 'working copy' repository + # if one doesn't already exist. Then the contents of the central SCM should be pulled into + # the working copy. + # + # The +to_identifier+ parameter may be optionally specified to obtain files up to a + # particular time or label. +to_identifier+ should either be a Time (in UTC - according to + # the clock on the SCM machine) or a String - reprsenting a label or revision. + # + # This method will yield the relative file name of each checked out file, and also return + # them in an array. Only files, not directories, should be yielded/returned. + # + # This method should be overridden for SCMs that are able to yield checkouts as they happen. + # For some SCMs this is not possible, or at least very hard. In that case, just override + # the checkout_silent method instead of this method (should be protected). + # + def checkout(to_identifier=Time.infinity, options={}) # :yield: file + to_identifier = Time.infinity if to_identifier.nil? + + before = checked_out_files + # We expect subclasses to implement this as a protected method (unless this whole method is overridden). + checkout_silent(to_identifier, options) + after = checked_out_files + + (after - before).sort! + end + + def checked_out_files + raise "checkout_dir not set" if @checkout_dir.nil? + + files = Dir["#{@checkout_dir}/**/*"] + files.delete_if{|file| File.directory?(file)} + ignore_paths.each do |regex| + files.delete_if{|file| file =~ regex} + end + dir = File.expand_path(@checkout_dir) + files.collect{|file| File.expand_path(file)[dir.length+1..-1]} + end + + # Returns a Revisions object for the interval specified by +from_identifier+ (exclusive, i.e. after) + # and optionally +:to_identifier+ (exclusive too). If +relative_path+ is specified, the result will only contain + # revisions pertaining to that path. + # + # For example, revisions(223, 229) should return revisions 224..228 + def revisions(from_identifier, options={}) + raise NotImplementedError + end + + # Opens a readonly IO to a file at +path+ + def open(path, native_revision_identifier, options={}, &block) #:yield: io + raise NotImplementedError + end + + # Whether the working copy is in synch with the central + # repository's revision/time identified by +identifier+. + # If +identifier+ is nil, 'HEAD' of repository should be assumed. + # + def uptodate?(identifier) + raise NotImplementedError + end + + # Whether the project is checked out from the central repository or not. + # Subclasses should override this to check for SCM-specific administrative + # files if appliccable + def checked_out? + File.exists?(@checkout_dir) + end + + # Whether triggers are supported by this SCM. A trigger is a command that can be executed + # upon a completed commit to the SCM. + def supports_trigger? + # The default implementation assumes no - override if it can be + # determined programmatically. + false + end + alias :can_install_trigger? :supports_trigger? + + # Descriptive name of the trigger mechanism + def trigger_mechanism + raise NotImplementedError + end + + # Installs +trigger_command+ in the SCM. + # The +install_dir+ parameter should be an empty local + # directory that the SCM can use for temporary files + # if necessary (CVS needs this to check out its administrative files). + # Most implementations will ignore this parameter. + # + def install_trigger(trigger_command, install_dir) + raise NotImplementedError + end + + # Uninstalls +trigger_command+ from the SCM. + # + def uninstall_trigger(trigger_command, install_dir) + raise NotImplementedError + end + + # Whether the command denoted by +trigger_command+ is installed in the SCM. + # + def trigger_installed?(trigger_command, install_dir) + raise NotImplementedError + end + + # The command line to run in order to check out a fresh working copy. + # + def checkout_commandline(to_identifier=Time.infinity) + raise NotImplementedError + end + + # The command line to run in order to update a working copy. + # + def update_commandline(to_identifier=Time.infinity) + raise NotImplementedError + end + + # Yields an IO containing the unified diff of the change. + # Also see RevisionFile#diff + def diff(path, from, to, options={}, &block) + raise NotImplementedError + end + + def ==(other_scm) + return false if self.class != other_scm.class + self.instance_variables.each do |var| + return false if self.instance_eval(var) != other_scm.instance_eval(var) + end + true + end + + # Whether or not to store the revision command in the Revisions instance returned by <tt>revisions</tt> + def store_revisions_command?; @store_revisions_command.nil? ? true : @store_revisions_command; end + + protected + + # Directory where commands must be run + def cmd_dir + nil + end + + # Wrapper for CommandLine.execute that provides default values for + # dir plus any options set in default_options (typically stdout and stderr). + def execute(cmd, options={}, &proc) + options = {:dir => cmd_dir}.merge(default_options).merge(options) + begin + CommandLine.execute(cmd, options, &proc) + rescue CommandLine::OptionError => e + e.message += "\nEither specify default_options on the scm object, or pass the required options to the method" + raise e + end + end + + end +end