ronin-rb/ronin-repos

View on GitHub
lib/ronin/repos/repository.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true
#
# Copyright (c) 2021-2024 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-repos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-repos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-repos.  If not, see <https://www.gnu.org/licenses/>.
#

require 'ronin/repos/exceptions'

require 'fileutils'
require 'time'

module Ronin
  module Repos
    #
    # Represents an installed repository.
    #
    # @api private
    #
    class Repository

      # The path to the repository's directory.
      #
      # @return [String]
      attr_reader :path

      # The name of the repository.
      #
      # @return [String]
      attr_reader :name

      #
      # Initializes the repository.
      #
      # @param [String] path
      #   The path to the repository.
      #
      # @raise [RepositoryNotFound]
      #   The path does not exist or does not point to a directory.
      #
      def initialize(path)
        @path = File.expand_path(path)

        unless File.exist?(@path)
          raise(RepositoryNotFound,"repository does not exist: #{@path.inspect}")
        end

        unless File.directory?(@path)
          raise(RepositoryNotFound,"path is not a directory: #{@path.inspect}")
        end

        @name = File.basename(@path)
      end

      #
      # Clones a repository.
      #
      # @param [String, URI::HTTPS] uri
      #   The `https://` or `git@HOST:PATH` SSH URI
      #
      # @param [String] path
      #   The path to where the repository will be cloned to.
      #
      # @param [Integer, nil] depth
      #   The number of commits to clone.
      #
      # @return [Repository]
      #   The newly cloned repository.
      #
      # @raise [CommandFailed]
      #   The `git` command failed.
      #
      # @raise [CommandNotFailed]
      #   The `git` command is not installed.
      #
      def self.clone(uri,path, depth: nil)
        path = path.to_s
        args = []

        if depth
          args << '--depth' << depth.to_s
        end

        args << uri.to_s
        args << path.to_s

        case system('git','clone',*args)
        when nil
          raise(CommandNotInstalled,"git is not installed")
        when false
          raise(CommandFailed,"command failed: git clone #{args.join(' ')}")
        end

        return new(path)
      end

      #
      # Clones and installs a repository from the URI and to the destination
      # path.
      #
      # @param [String, URI::HTTPS] uri
      #   The `https://` or `git@HOST:PATH` SSH URI
      #
      # @param [String] path
      #   The path to where the repository will be cloned to.
      #
      # @param [String, nil] branch
      #   The git branch to pull.
      #
      # @param [Boolean] tag
      #   Controls whether to pull git tags in addition to the git commits.
      #
      # @param [Hash{Symbol => Object}] kwargs
      #   Additional keyword arguments for {clone}.
      #
      # @return [Repository]
      #   The newly cloned repository.
      #
      # @raise [CommandFailed]
      #
      def self.install(uri,path, branch: nil, tag: nil, **kwargs)
        repo = clone(uri,path, **kwargs)

        if branch || tag
          repo.checkout(branch || tag)
        end

        return repo
      end

      #
      # The git URL of the repository.
      #
      # @return [String]
      #   The `git:` or `https://` URL for the repository.
      #
      # @since 0.2.0
      #
      def url
        Dir.chdir(@path) do
          `git remote get-url origin`.chomp
        end
      end

      #
      # Determines when the repository was last updated.
      #
      # @return [Time]
      #   The timestamp of the last commit will be returned.
      #
      # @since 0.2.0
      #
      def last_updated_at
        Dir.chdir(@path) do
          Time.parse(`git log --date=iso8601 --pretty="%cd" -1`)
        end
      end

      #
      # Pulls down new git commits.
      #
      # @param [String] remote
      #   The git remote to pull from.
      #
      # @param [String, nil] branch
      #   The git branch to pull.
      #
      # @param [Boolean] tags
      #   Controls whether to pull git tags in addition to the git commits.
      #
      # @return [true]
      #   Indicates that the `git` command executed successfully.
      #
      # @raise [CommandFailed]
      #
      def pull(remote: 'origin', branch: nil, tags: nil)
        args = []
        args << '--tags' if tags

        args << remote.to_s
        args << branch.to_s if branch

        Dir.chdir(@path) do
          case system('git','pull',*args)
          when nil
            raise(CommandNotInstalled,"git is not installed")
          when false
            raise(CommandFailed,"command failed: git pull #{args.join(' ')}")
          end
        end
      end

      #
      # Checks out the  git branch or tag.
      #
      # @param [String] branch_or_tag
      #   The branch or tag name to checkout.
      #
      # @return [true]
      #   Indicates that the `git` command executed successfully.
      #
      # @raise [CommandFailed]
      #
      def checkout(branch_or_tag)
        Dir.chdir(@path) do
          case system('git','checkout',branch_or_tag)
          when nil
            raise(CommandNotInstalled,"git is not installed")
          when false
            raise(CommandFailed,"command failed: git checkout #{branch_or_tag}")
          end
        end
      end

      #
      # Updates the repository.
      #
      # @param [String, nil] branch
      #   The optional git branch to update from.
      #
      # @param [String, nil] tag
      #   The optional git tag to update to.
      #
      # @param [Hash{Symbol => Object}] kwargs
      #   Additional keyword arguments for {#pull}.
      #
      # @return [true]
      #   Indicates that the `git` commands executed successfully.
      #
      # @raise [CommandFailed]
      #   One of the `git` commands failed.
      #
      def update(branch: nil, tag: nil, **kwargs)
        pull(branch: branch, tags: branch.nil?, **kwargs)

        if branch || tag
          checkout(branch || tag)
        end
      end

      #
      # Deletes the repository directory.
      #
      def delete
        FileUtils.rm_rf(@path)
      end

      #
      # Converts a relative path to an absolute path.
      #
      # @param [String] relative_path
      #   The relative path of the file.
      #
      # @return [String]
      #   The absolute path with respect to the repository.
      #
      def join(relative_path)
        File.join(@path,relative_path)
      end

      #
      # Determines if the repository contains the file.
      #
      # @param [String] relative_path
      #   The relative path of the file.
      #
      # @return [Boolean]
      #   Indicates whether the repository contains the file or not.
      #
      def has_file?(relative_path)
        File.file?(join(relative_path))
      end

      #
      # Determines if the repository contains the directory.
      #
      # @param [String] relative_path
      #   The relative path of the directory.
      #
      # @return [Boolean]
      #   Indicates whether the repository contains the directory or not.
      #
      def has_directory?(relative_path)
        File.directory?(join(relative_path))
      end

      #
      # Finds a file within the repository.
      #
      # @param [String] relative_path
      #   The relative path of the file.
      #
      # @return [String, nil]
      #   The absolute path of the matching file or `nil` if no matching file
      #   could be found.
      #
      # @example
      #   repo.find_file("wordlists/wordlist.txt")
      #   # => "/home/user/.cache/ronin-repos/foo-repo/wordlists/wordlist.txt"
      #
      def find_file(relative_path)
        path = join(relative_path)

        if File.file?(path)
          return path
        end
      end

      #
      # Finds all files in the repository that matches the glob pattern.
      #
      # @param [String] pattern
      #   The file glob pattern to search for.
      #
      # @return [Array<String>]
      #   The absolute paths to the files that match the glob pattern.
      #
      # @example
      #   repo.glob("wordlists/*.txt")
      #   # => ["/home/user/.cache/ronin-repos/foo-repo/wordlists/cities.txt",
      #   #     "/home/user/.cache/ronin-repos/foo-repo/wordlists/states.txt"]
      #
      def glob(pattern,&block)
        path    = join(pattern)
        matches = Dir.glob(path)

        if block then matches.each(&block)
        else          matches
        end
      end

      #
      # Lists the paths within the repository.
      #
      # @param [String] pattern
      #   The optional glob pattern to use to list specific files.
      #
      # @return [Array<String>]
      #   The matching paths within the repository.
      #
      # @example
      #   repo.list_files('exploits/{**/}*.rb')
      #   # => ["exploits/exploit1.rb", "exploits/exploit2.rb"]
      #
      def list_files(pattern='{**/}*.*')
        Dir.glob(pattern, base: @path)
      end

      #
      # Converts the repository to a String.
      #
      # @return [String]
      #   The name of the repository.
      #
      def to_s
        @name
      end

    end
  end
end