Beginning a CloudForge Git client using Thor and Interact gems

One of the advantages to blogging is the opportunity to play with and write about technologies that I don’t normally interact with day-to-day. This is especially true of many of the great gems available for Ruby. Building on Jack’s blog post about extending CloudForge via the API I decided to play with a few gems I haven’t had a chance to play with yet: Thor and Interact.

gem install thor interact

Thor is an awesome toolkit for building robust command-line interfaces (CLIs). It’s somewhat similar to Rake but provides more structure specific for DRY command-line scripting. It’s also an essential part of Bundler and Rails under-the-covers.

Interact provides an API for interacting with users beyond simply parsing ARGV. It allows a CLI to ask multiple-choice questions, prompt for passwords, set defaults, and even rewind questions. It is part of the VMC client used to configure and interact with CloudFoundry. The combination of these two gems provides a great deal of flexibility for user-interaction.

My goal in this blog is to create a simple CLI that integrates Thor, Interact and the CloudForge API, making it possible to clone Git repositories by choosing from a list of CloudForge projects.  Git is pretty simple to use as it is but this program provides a good chance to demonstrate these gems and our API. I’ll include a few other gems as well to make things a little easier. I’m also going to start my script with a ruby directive that automatically invokes my script within Ruby when executed.

#!/usr/bin/env ruby
require 'thor'
require 'interact'
require 'yaml'
require 'rest-client'
require 'json'

With my required gems in-place I’m going to start with a simple class method called “config” to save my configuration settings. This will save my CloudForge credentials to a configuration file in my HOME directory so I won’t need to enter them every time.

class CFGit < Thor
  
  # Set up a configuration file so credentials don't need to be entered again.
  desc "config", "configure the domain, username and password"
  option :domain, :aliases => "-d"
  option :username, :aliases => "-u"
  option :password, :aliases => "-pw"
  
  def config 
    # Open a configuration file in the user's home directory
    open(ENV['HOME']+'/.cfgit_config', 'w') do |f|
      # Throw the configuration settings into the fiile using Yaml
      f.write Parameters.new.param_config(options).to_yaml
    end
end

With these few lines Thor automatically creates a ‘config’ command with option flags and provides help documentation.

Config Command and Help

Thor provides full help context and option flags

This would work as-is except I am calling a “Parameters” class to set my options in my Yaml file. My Parameters class interactively questions the user if any configuration settings are not included.

class Parameters
  include Interactive
  
  def param_config(options)
    
    cf_config=options.dup
    cf_config[:domain] ||= ask "Account Domain"
    cf_config[:username] = ask "Username"
    cf_config[:password] = ask "Password"
    
    return cf_config
      
  end

Now, if I call ‘cfgit config’ I can optionally set my Domain, Username, and Password as command arguments or the program will interactively prompt me to enter them.

Config Interact

Interact prompts the user to enter values for the parameters

This saves my settings into the “cfgit_config” file in my HOME directory. Rather than saving my password locally in the file I created User API Key on my Settings page in CloudForge. This key is used only for API calls and replaces my password. I also want password storage to be optional so I’ll make Password an option for all command arguments.

Now, I want this application to actually do something other than just saving parameters to a file. I’m going to add a “clone” function that will allow the user to select a CloudForge project from a list of all projects, retrieve the ‘ssh’ url for the Git repository in that project and finally clone that Git repository from CloudForge into the present working directory.

First, I need to add a “clone” function to my main Thor class that allows me override any pre-set configuration credentials. i.e. I can skip adding my domain, username, or password into my configuration file and enter them each time I run clone.

# clone a specific repository to the current directory
desc "clone", "clone a repository"
option :project, :aliases => "-p"
option :branch, :aliases => "-b", :default => 'master'
option :domain, :aliases => "-d"
option :username, :aliases => "-u"
option :password, :aliases => "-pw"

	def clone
		# Make sure that all parameters are available
		clone_options = Parameters.new.param_clone(options)

		# get the git ssh url for the selected project
		git_url = CF_Client.git_url(clone_options)

		unless git_url.nil?
  			`git clone #{git_url} -b #{clone_options[:branch]}`
		else
  			puts "This project does not contain a Git repository"
		end
	end
end

I’m still relying on my Parameters class to validate that all of the required parameters are available so I’ll have to add that method to my Parameters class.

def param_clone(options)

	cf_config = YAML::load(File.open(ENV['HOME']+'/.cfgit_config'))
	clone_options = options.dup

	clone_options[:domain] ||= cf_config[:domain]
	clone_options[:username] ||= cf_config[:username]
	clone_options[:password] ||= cf_config[:password]
	clone_options[:project] ||= projects(clone_options[:domain], clone_options[:username], clone_options[:password])
	return clone_options

end

def projects (domain, username, password)

	project_list = ask "Please pick a project",
	:choices => CF_Client.list_projects(domain, username, password),
	:indexed => true

end
end

Just for fun, I added a second method that uses Interact and the CloudForge API to list all available projects as an indexed multiple-choice list. This works when there are few projects available in the account but I wouldn’t recommend it with a long list of projects. My client for the CloudForge API is pretty simple with just two methods:

module CF_Client
  
  DEVORG = 'spiderpig'
  DEVKEY = '1e38bb5ffcf2fc08cff3800eb286fd4eaad60dea'
  URL = "https://api.cloudforge.com/api/1/"
  
  def self.list_projects(domain, username, password)
	projects=[]
	projects = JSON.parse (RestClient.get "#{URL}projects.json", {:params => {"credentials[developerOrganization]" => DEVORG, 
                                    "credentials[developerKey]" => DEVKEY,
                                    "credentials[domain]" => domain,
                                    "credentials[login]" => username,
                                    "credentials[password]" => password}})
    
    project_names = projects.map{|p| p["shortName"]}
	
  end
  
  def self.git_url(options)
    quick_link = nil
    services = JSON.parse (RestClient.get "#{URL}services.json", {:params => {"credentials[developerOrganization]" => DEVORG, 
                                    "credentials[developerKey]" => DEVKEY,
                                    "credentials[domain]" => options[:domain],
                                    "credentials[login]" => options[:username],
                                    "credentials[password]" => options[:password]}})
  
    services.each do |srv|
      if /#{options[:project].to_s}/ =~ srv["accessUrl"]["ssh"] 
        quick_link = srv["accessUrl"]["ssh"]
      end
    end
    return quick_link
  end
end
Clone command

A multiple-choice list of projects to clone

The last step is to simply choosing project by name or number and it is immediately cloned into my working directory. This application can be easily extended to perform many other features with CloudForge and Git such as “Forking” repositories from one Project to another, creating new Git repositories, or anything else you can think up. I had fun creating this sample application and I hope it helps someone to extend their CloudForge usage or simply start using Thor or Interact.

Below is the full source of this application. You will need to change the DEVORG and DEVKEY constants to your own settings to make it work. If you don’t already have a Git repository on CloudForge click HERE and get started today.

#!/usr/bin/env ruby
require 'thor'
require 'interact'
require 'yaml'
require 'rest-client'
require 'json'


# A chance to use the Thor gem for user interaction
class CFGit  "-d"
  option :username, :aliases => "-u"
  option :password, :aliases => "-pw"
  
  def config 
    # Open a configuration file in the user's home directory
    open(ENV['HOME']+'/.cfgit_config', 'w') do |f|
      # Throw the configuration settings into the fiile using Yaml
      f.write Parameters.new.param_config(options).to_yaml
    end
  end
  
  # clone a specific repository to the current directory
  desc "clone", "clone a repository"
  option :project, :aliases => "-p"
  option :branch, :aliases => "-b", :default => 'master'
  option :domain, :aliases => "-d"
  option :username, :aliases => "-u"
  option :password, :aliases => "-pw"
  
  def clone
    # Make sure that all parameters are available
    clone_options = Parameters.new.param_clone(options)
    
    # get the git ssh url for the selected project
    git_url = CF_Client.git_url(clone_options)
    
    unless git_url.nil?
      `git clone #{git_url} -b #{clone_options[:branch]}`
    else
      puts "This project does not contain a Git repository"
    end
  end
end

class Parameters
  include Interactive
  
  def param_config(options)
    
    cf_config=options.dup
    cf_config[:domain] ||= ask "Account Domain"
    cf_config[:username] = ask "Username"
    cf_config[:password] = ask "Password"
	return cf_config
      
  end
  
  def param_clone(options)
    
    cf_config = YAML::load(File.open(ENV['HOME']+'/.cfgit_config'))
    clone_options = options.dup
	clone_options[:domain] ||= cf_config[:domain]
    clone_options[:username] ||= cf_config[:username]
    clone_options[:password] ||= cf_config[:password]
	clone_options[:project] ||= projects(clone_options[:domain], clone_options[:username], clone_options[:password])
	return clone_options
    
  end
  
  def projects (domain, username, password)
    
    project_list = ask "Please pick a project",
    :choices => CF_Client.list_projects(domain, username, password),
    :indexed => true
    
  end
  
end

module CF_Client
  
  DEVORG = 'spiderpig'
  DEVKEY = '1e38bb5ffcf1ec08daf3800eg286fd4eaad60dea'
  URL = "https://api.cloudforge.com/api/1/"
  
  def self.list_projects(domain, username, password)
	projects=[]
	projects = JSON.parse (RestClient.get "#{URL}projects.json", {:params => {"credentials[developerOrganization]" => DEVORG, 
                                    "credentials[developerKey]" => DEVKEY,
                                    "credentials[domain]" => domain,
                                    "credentials[login]" => username,
                                    "credentials[password]" => password}})
    
    project_names = projects.map{|p| p["shortName"]}
  end
  
  def self.git_url(options)
	quick_link = nil
    services = JSON.parse (RestClient.get "#{URL}services.json", {:params => {"credentials[developerOrganization]" => DEVORG, 
                                    "credentials[developerKey]" => DEVKEY,
                                    "credentials[domain]" => options[:domain],
                                    "credentials[login]" => options[:username],
                                    "credentials[password]" => options[:password]}})
  
    services.each do |srv|
      if /#{options[:project].to_s}/ =~ srv["accessUrl"]["ssh"] 
        quick_link = srv["accessUrl"]["ssh"]
      end
    end
    return quick_link
  end
end

CFGit.start(ARGV)
Patrick Wolf

Currently a Sr. Product Manager at CollabNet, I have worked in enterprise and SaaS software for 15+ years. In that time I have seen the migration from traditional waterfall to water-scrum-fall to full agile development across many distributed teams.

Tagged with: , , , , , , , , , , , , , , ,
Posted in CloudForge, Git

Leave a Reply

Your email address will not be published. Required fields are marked *

*

CAPTCHA Image

*

connect with CollabNet
   Contact Us
Subscribe

Have new blog posts sent directly to your email.

looking for something
conversations

CloudForge: Join #CollabNet for the TeamForge® 8.1 release webinar and learn about its new powerful enterprise #Git features http://t.co/IHfnkoEfGr
Date: 1 September 2015 | 5:00 pm

CloudForge: Join this #CollabNet #webinar and learn how to reduce server loads with #Git replication and improve Git performance http://t.co/pB1DEsWFPh
Date: 31 August 2015 | 6:00 pm

CloudForge: Seamlessly integrate #Git upstream and downstream to tools such as #Jira and #Jenkins on this #CollabNet #webinar http://t.co/pB1DEsWFPh
Date: 28 August 2015 | 5:30 pm