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: # # * :stdout: Path to file name where stdout of SCM operations are written. # * :stdout: 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 :dir into the central scm, # using commit message :message 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