Commit eacecc8e authored by Fabio Pelosin's avatar Fabio Pelosin

Merge pull request #205 from CocoaPods/listNew

Pod list new & RSS
parents 421ddf80 237185b5
......@@ -3,9 +3,9 @@ require 'colored'
module Pod
class Command
autoload :ErrorReport, 'cocoapods/command/error_report'
autoload :SetPresent, 'cocoapods/command/set_present'
autoload :Install, 'cocoapods/command/install'
autoload :List, 'cocoapods/command/list'
autoload :Presenter, 'cocoapods/command/presenter'
autoload :Repo, 'cocoapods/command/repo'
autoload :Search, 'cocoapods/command/search'
autoload :Setup, 'cocoapods/command/setup'
......
......@@ -2,101 +2,68 @@ module Pod
class Command
class List < Command
def self.banner
%{List all pods:
%{List all pods:
$ pod list
Lists all available pods.
$ pod list [DAYS]
$ pod list new
Lists the pods introduced in the master repo since the given number of days.}
Lists the pods introduced in the master repository since the last check.}
end
def self.options
SetPresent.set_present_options +
super
" --update runs `pod repo update` before list\n" +
Presenter.options + super
end
include SetPresent
extend Executable
executable :git
def initialize(argv)
parse_set_options(argv)
@days = argv.arguments.first
unless @days == nil || @days =~ /^[0-9]+$/
super
end
end
def dir
config.repos_dir + 'master'
end
def dir_list_from_commit(commit)
Dir.chdir(dir) { git("ls-tree --name-only -r #{commit}") }
end
def commit_from_days_ago (days)
Dir.chdir(dir) { git("rev-list -n1 --before=\"#{days} day ago\" --first-parent master") }
end
def spec_names_from_commit (commit)
dir_list = dir_list_from_commit(commit)
# Keep only subdirectories
dir_list.gsub!(/^[^\/]*$/,'')
# Keep only subdirectories name
dir_list.gsub!(/(.*)\/[0-9].*/,'\1')
result = dir_list.split("\n").uniq
result.delete('')
result
@update = argv.option('--update')
@new = argv.option('new')
@presenter = Presenter.new(argv)
super unless argv.empty?
end
def new_specs_set(commit)
#TODO: find the changes for all repos
new_specs = spec_names_from_commit('HEAD') - spec_names_from_commit(commit)
sets = all_specs_set.select { |set| new_specs.include?(set.name) }
end
def all_specs_set
result = []
Source.all.each do |source|
source.pod_sets.each do |set|
result << set
end
end
result
def list_all
sets = Source.all_sets
sets.each {|s| puts @presenter.describe(s)}
puts "\n#{sets.count} pods were found"
end
def list_new
sets = new_specs_set(commit_from_days_ago(@days))
present_sets(sets)
if !list
if sets.count != 0
puts "#{sets.count} new pods were added in the last #{@days} days"
puts
else
puts "No new pods were added in the last #{@days} days"
puts
days = [1,2,3,5,8]
dates, groups = {}, {}
days.each {|d| dates[d] = Time.now - 60 * 60 * 24 * d}
sets = Source.all_sets
creation_dates = Pod::Specification::Statistics.instance.creation_dates(sets)
sets.each do |set|
set_date = creation_dates[set.name]
days.each do |d|
if set_date >= dates[d]
groups[d] = [] unless groups[d]
groups[d] << set
break
end
end
end
end
def list_all
present_sets(all_specs_set)
puts "#{all_specs_set.count} pods were found"
puts
days.reverse.each do |d|
sets = groups[d]
next unless sets
puts "\nPods added in the last #{d == 1 ? 'day' : "#{d} days"}".yellow
sets.sort_by {|s| creation_dates[s.name]}.each {|s| puts @presenter.describe(s)}
end
end
def run
if @days
list_new
else
list_all
end
puts "\nUpdating Spec Repositories\n".yellow if @update && config.verbose?
Repo.new(ARGV.new(["update"])).run if @update
@new ? list_new : list_all
puts
end
end
end
......
module Pod
class Command
class Presenter
def self.options
" --stats Show additional stats (like GitHub watchers and forks)\n"
end
autoload :CocoaPod, 'cocoapods/command/presenter/cocoa_pod'
def initialize(argv)
@stats = argv.option('--stats')
end
def render(array)
result = "\n"
seats.each {|s| puts describe(s)}
result
end
def describe(set)
pod = CocoaPod.new(set)
result = "\n--> #{pod.name} (#{pod.versions})\n".green
result << wrap_string(pod.summary)
result << detail('Homepage', pod.homepage)
result << detail('Source', pod.source_url)
result << detail('Authors', pod.authors) if @stats && pod.authors =~ /,/
result << detail('Author', pod.authors) if @stats && pod.authors !~ /,/
result << detail('Platform', pod.platform) if @stats
result << detail('License', pod.license) if @stats
result << detail('Watchers', pod.github_watchers) if @stats
result << detail('Forks', pod.github_forks) if @stats
result
end
private
# adapted from http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/
def wrap_string(txt, col = 80, indentation = 4)
indent = ' ' * indentation
txt.strip.gsub(/(.{1,#{col}})( +|$)\n?|(.{#{col}})/, indent + "\\1\\3\n")
end
def detail(title, string, preferred_indentation = 8)
# 8 is the length of Homepage
return '' if !string
number_of_spaces = ((preferred_indentation - title.length) > 0) ? (preferred_indentation - title.length) : 0
spaces = ' ' * number_of_spaces
" - #{title}: #{spaces + string}\n"
end
end
end
end
module Pod
class Command
class Presenter
class CocoaPod
def initialize(set)
@set = set
end
# set information
def name
@set.name
end
def version
@set.versions.last
end
def versions
@set.versions.reverse.join(", ")
end
# specification information
def spec
@spec ||= @set.specification.part_of_other_pod? ? @set.specification.part_of_specification : @set.specification
end
def authors
oxfordify spec.authors.keys
end
def homepage
spec.homepage
end
def description
spec.description
end
def summary
spec.summary
end
def source_url
spec.source.reject {|k,_| k == :commit || k == :tag }.values.first
end
def platform
spec.platform.to_s
end
def license
spec.license[:type] if spec.license
end
# Statistics information
def creation_date
Pod::Specification::Statistics.instance.creation_date(@set)
end
def github_watchers
Pod::Specification::Statistics.instance.github_watchers(@set)
end
def github_forks
Pod::Specification::Statistics.instance.github_forks(@set)
end
private
def oxfordify words
if words.size < 3
words.join ' and '
else
"#{words[0..-2].join(', ')}, and #{words.last}"
end
end
end
end
end
end
......@@ -13,23 +13,20 @@ module Pod
def self.options
" --full Search by name, summary, and description\n" +
SetPresent.set_present_options +
super
Presenter.options + super
end
include SetPresent
def initialize(argv)
parse_set_options(argv)
@full_text_search = argv.option('--full')
unless @query = argv.arguments.first
super
end
@presenter = Presenter.new(argv)
@query = argv.shift_argument
super unless argv.empty? && @query
end
def run
sets = Source.search_by_name(@query.strip, @full_text_search)
present_sets(sets)
sets.each {|s| puts @presenter.describe(s)}
puts
end
end
end
......
require 'net/http'
module Pod
class Command
module SetPresent
def self.set_present_options
" --name-only Show only the names of the pods\n" +
" --stats Show additional stats (like GitHub watchers and forks)\n"
end
def list
@list
end
def parse_set_options(argv)
@stats = argv.option('--stats')
@list = argv.option('--name-only')
end
def present_sets(array)
array.each do |set|
present_set(set)
end
end
def present_set(set)
if @list
puts set.name
else
puts "--> #{set.name} (#{set.versions.reverse.join(", ")})".green
puts_wrapped_text(set.specification.summary)
spec = set.specification.part_of_other_pod? ? set.specification.part_of_specification : set.specification
source = spec.source.reject {|k,_| k == :commit || k == :tag }.values.first
puts_detail('Homepage', spec.homepage)
puts_detail('Source', source)
if @stats
stats = stats(source)
puts_detail('Watchers', stats[:watchers])
puts_detail('Forks', stats[:forks])
end
puts
end
end
# adapted from http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/
def puts_wrapped_text(txt, col = 80, indentation = 4)
indent = ' ' * indentation
puts txt.strip.gsub(/(.{1,#{col}})( +|$)\n?|(.{#{col}})/, indent + "\\1\\3\n")
end
def puts_detail(title,string)
return if !string
# 8 is the length of homepage
number_of_spaces = ((8 - title.length) > 0) ? (8 - title.length) : 0
spaces = ' ' * number_of_spaces
puts " - #{title}: #{spaces + string}"
end
def stats(url)
original_url, username, reponame = *(url.match(/[:\/]([\w\-]+)\/([\w\-]+)\.git/).to_a)
result = {}
if original_url
gh_response = Net::HTTP.get('github.com', "/api/v2/json/repos/show/#{username}/#{reponame}")
result[:watchers] = gh_response.match(/\"watchers\"\W*:\W*([0-9]+)/).to_a[1]
result[:forks] = gh_response.match(/\"forks\"\W*:\W*([0-9]+)/).to_a[1]
end
result
end
end
end
end
......@@ -167,6 +167,7 @@ module Pod
warnings << 'Missing license[:type]' unless spec.license && spec.license[:type]
warnings << 'Missing license[:file] or [:text]' unless spec.license && (spec.license[:file] || spec.license[:text])
warnings << "Github repositories should end in `.git'" if spec.source[:git] =~ /github.com/ && spec.source[:git] !~ /.*\.git/
warnings << "Github repositories should start with https://github.com" if spec.source[:git] =~ /git:\/\/github\.com/
unless warnings.empty?
......
module Pod
class Platform
attr_reader :options
def initialize(symbolic_name, options = {})
@symbolic_name = symbolic_name
@options = options
end
def name
@symbolic_name
end
def ==(other_platform_or_symbolic_name)
if other_platform_or_symbolic_name.is_a?(Symbol)
@symbolic_name == other_platform_or_symbolic_name
......@@ -18,25 +18,32 @@ module Pod
self == (other_platform_or_symbolic_name.name)
end
end
def to_s
name.to_s
case @symbolic_name
when :ios
'iOS'
when :osx
'OS X'
else
'iOS - OS X'
end
end
def to_sym
name
end
def nil?
name.nil?
end
def deployment_target
if (opt = options[:deployment_target])
Pod::Version.new(opt)
end
end
def requires_legacy_ios_archs?
return unless deployment_target
(name == :ios) && (deployment_target < Pod::Version.new("4.3"))
......
......@@ -12,6 +12,10 @@ module Pod
end
end
def all_sets
all.map {|source| source.pod_sets}.flatten
end
def search(dependency)
all.map { |s| s.search(dependency) }.compact.first ||
raise(Informative, "[!] Unable to find a pod named `#{dependency.name}'".red)
......@@ -32,6 +36,10 @@ module Pod
Aggregate.new.all
end
def self.all_sets
Aggregate.new.all_sets
end
def self.search(dependency)
Aggregate.new.search(dependency)
end
......
......@@ -9,7 +9,8 @@ module Pod
end
class Specification
autoload :Set, 'cocoapods/specification/set'
autoload :Set, 'cocoapods/specification/set'
autoload :Statistics, 'cocoapods/specification/statistics'
# The file is expected to define and return a Pods::Specification.
def self.from_file(path)
......@@ -126,6 +127,7 @@ module Pod
def header_dir=(dir)
@header_dir = Pathname.new(dir)
end
def header_dir
@header_dir || pod_destroot_name
end
......
require 'net/https'
require 'uri'
require 'yaml'
module Pod
class Specification
class Statistics
def self.instance
@instance ||= new
end
def self.instance=(instance)
@instance = instance
end
attr_accessor :cache_file, :cache_expiration
def initialize
@cache_file = Config.instance.repos_dir + 'statistics.yml'
@cache_expiration = 60 * 60 * 24 * 3
end
def creation_date(set)
compute_creation_date(set)
end
def creation_dates(sets)
dates = {}
sets.each { |set| dates[set.name] = compute_creation_date(set, false) }
save_cache
dates
end
def github_watchers(set)
github_stats_if_needed(set)
get_value(set, :gh_watchers)
end
def github_forks(set)
github_stats_if_needed(set)
get_value(set, :gh_forks)
end
private
def cache
@cache ||= cache_file && cache_file.exist? ? YAML::load(cache_file.read) : {}
end
def get_value(set, key)
if cache[set.name] && cache[set.name][key]
cache[set.name][key]
end
end
def set_value(set, key, value)
cache[set.name] ||= {}
cache[set.name][key] = value
end
def save_cache
File.open(cache_file, 'w') { |f| f.write(YAML::dump(cache)) } if cache_file
end
def compute_creation_date(set, save = true)
date = get_value(set, :creation_date)
unless date
Dir.chdir(set.pod_dir.dirname) do
date = Time.at(`git log --first-parent --format=%ct #{set.name}`.split("\n").last.to_i)
end
set_value(set, :creation_date, date)
end
save_cache if save
date
end
def github_stats_if_needed(set)
return if get_value(set, :gh_date) && get_value(set, :gh_date) > Time.now - cache_expiration
spec = set.specification.part_of_other_pod? ? set.specification.part_of_specification : set.specification
url = spec.source.reject {|k,_| k == :commit || k == :tag }.values.first
gh_url, username, reponame = *(url.match(/[:\/]([\w\-]+)\/([\w\-]+)\.git/).to_a)
return unless gh_url
response_body = fetch_stats(username, reponame)
return unless response_body
watchers = response_body.match(/"watchers"\W*:\W*([0-9]+)/).to_a[1]
forks = response_body.match(/"forks"\W*:\W*([0-9]+)/).to_a[1]
return unless watchers && forks
cache[set.name] ||= {}
set_value(set, :gh_watchers, watchers)
set_value(set, :gh_forks, forks)
set_value(set, :gh_date, Time.now)
save_cache
end
def fetch_stats(username, reponame)
uri = URI.parse("https://api.github.com/repos/#{username}/#{reponame}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Get.new(uri.request_uri)
response = http.request(request)
response.body if response.is_a?(Net::HTTPSuccess)
end
end
end
end
......@@ -13,35 +13,43 @@ describe "Pod::Command::List" do
def command(arguments = argv)
command = Pod::Command::List.new(arguments)
def command.puts(msg = '')
(@printed ||= '') << "#{msg}\n"
end
command
end
it "it accepts corret inputs and runs without errors" do
lambda { command().run }.should.not.raise
lambda { command(argv('10')).run }.should.not.raise
it "runs with correct parameters" do
lambda { command.run }.should.not.raise
lambda { command(argv('new')).run }.should.not.raise
end
it "complains if the days parameter is not a number" do
lambda { command(argv('10a')).run }.should.raise Pod::Command::Help
it "complains for wrong parameters" do
lambda { command(argv('wrong')).run }.should.raise Pod::Command::Help
lambda { command(argv('--wrong')).run }.should.raise Pod::Command::Help
end
it "returns the specs know in a given commit" do
specs = command(argv('10')).spec_names_from_commit('cad98852103394951850f89f0efde08f9dc41830')
specs[00].should == 'A2DynamicDelegate'
specs[10].should == 'DCTTextFieldValidator'
specs[20].should == 'INKeychainAccess'
specs[30].should == 'MKNetworkKit'
it "presents the known pods" do
list = command()
list.run
[ 'ZBarSDK',
'TouchJSON',
'SDURLCache',
'MagicalRecord',
'A2DynamicDelegate',
'75 pods were found'
].each {|s| list.output.should.include s }
end
it "returns the new specs introduced after a given commit" do
new_specs = command(argv('10')).new_specs_set('1c138d254bd39a3ccbe95a720098e2aaad5c5fc1')
new_specs_name = new_specs.map { |spec| spec.name }
new_specs_name.should.include 'iCarousel'
new_specs_name.should.include 'libPusher'
it "returns the new pods" do
Time.stubs(:now).returns(Time.mktime(2012,2,3))
list = command(argv('new'))
list.run
[ 'iCarousel',
'libPusher',
'SSCheckBoxView',
'KKPasscodeLock',
'SOCKit',
'FileMD5Hash',
'cocoa-oauth',
'iRate'
].each {|s| list.output.should.include s }
end
end
......
require File.expand_path('../../../spec_helper', __FILE__)
require 'net/http'
describe Pod::Command::SetPresent do
describe Pod::Command::Presenter do
Presenter = Pod::Command::Presenter
before do
@set = Pod::Spec::Set.new(fixture('spec-repos/master/CocoaLumberjack'))
@dummy = Object.new
@dummy.extend(Pod::Command::SetPresent)
def @dummy.puts(msg = '') (@printed ||= '') << "#{msg}\n" end
def @dummy.prinded() @printed.chomp end
end
it "repects the `--name-only' option" do
@dummy.parse_set_options(argv('--name-only'))
@dummy.present_set(@set)
@dummy.prinded.should == 'CocoaLumberjack'
Pod::Specification::Statistics.instance.cache_file = nil
end
it "presents the name, version, description, homepage and source of a specification set" do
@dummy.parse_set_options(argv())
@dummy.present_set(@set)
@dummy.prinded.should.include? 'CocoaLumberjack'
@dummy.prinded.should.include? '1.0'
@dummy.prinded.should.include? '1.1'
@dummy.prinded.should.include? 'A fast & simple, yet powerful & flexible logging framework for Mac and iOS.'
@dummy.prinded.should.include? 'https://github.com/robbiehanson/CocoaLumberjack'
@dummy.prinded.should.include? 'https://github.com/robbiehanson/CocoaLumberjack.git'
presenter = Presenter.new(argv())
output = presenter.describe(@set)
output.should.include? 'CocoaLumberjack'
output.should.include? '1.0'
output.should.include? '1.1'
output.should.include? 'A fast & simple, yet powerful & flexible logging framework for Mac and iOS.'
output.should.include? 'https://github.com/robbiehanson/CocoaLumberjack'
output.should.include? 'https://github.com/robbiehanson/CocoaLumberjack.git'
end
it "presents the stats of a specification set" do
response = '{"repository":{"homepage":"","url":"https://github.com/robbiehanson/CocoaLumberjack","has_downloads":true,"has_issues":true,"language":"Objective-C","master_branch":"master","forks":42,"fork":false,"created_at":"2011/03/30 19:38:39 -0700","has_wiki":true,"description":"A fast & simple, yet powerful & flexible logging framework for Mac and iOS","size":416,"private":false,"name":"CocoaLumberjack","owner":"robbiehanson","open_issues":4,"watchers":318,"pushed_at":"2012/03/26 12:39:36 -0700"}}% '
Net::HTTP.expects(:get).with('github.com', '/api/v2/json/repos/show/robbiehanson/CocoaLumberjack').returns(response)
@dummy.parse_set_options(argv('--stats'))
@dummy.present_set(@set)
@dummy.prinded.should.match(/Watchers:\W+[0-9]+/)
@dummy.prinded.should.match(/Forks:\W+[0-9]+/)
Pod::Specification::Statistics.instance.expects(:fetch_stats).with("robbiehanson", "CocoaLumberjack").returns(response)
presenter = Presenter.new(argv('--stats'))
output = presenter.describe(@set)
output.should.include? 'Author: Robbie Hanson'
output.should.include? 'License: BSD'
output.should.include? 'Platform: iOS - OS X'
output.should.include? 'Watchers: 318'
output.should.include? 'Forks: 42'
end
end
require File.expand_path('../../../spec_helper', __FILE__)
describe "Pod::Command::Search" do
extend SpecHelper::Git
before do
config.repos_dir = fixture('spec-repos')
end
after do
config.repos_dir = tmp_repos_path
end
def command(arguments = argv)
command = Pod::Command::Search.new(arguments)
end
it "runs with correct parameters" do
lambda { command(argv('table')).run }.should.not.raise
lambda { command(argv('table','--full')).run }.should.not.raise
end
it "complains for wrong parameters" do
lambda { command(argv('too','many')).run }.should.raise Pod::Command::Help
lambda { command(argv('too','--wrong')).run }.should.raise Pod::Command::Help
lambda { command(argv('--missing_query')).run }.should.raise Pod::Command::Help
end
it "presents the search results" do
search = command(argv('table'))
search.run
output = search.output
output.should.include 'EGOTableViewPullRefresh'
end
end
......@@ -66,3 +66,11 @@ VCR.configure do |c|
c.hook_into :webmock # or :fakeweb
c.allow_http_connections_when_no_cassette = true
end
class Pod::Command
attr_accessor :output
def puts(msg = '') (@output ||= '') << "#{msg}\n" end
end
Pod::Specification::Statistics.instance.cache_file = nil
......@@ -10,15 +10,15 @@ module SpecHelper
ROOT + 'tmp'
end
module_function :temporary_directory
def setup_temporary_directory
temporary_directory.mkpath
end
def teardown_temporary_directory
temporary_directory.rmtree if temporary_directory.exist?
end
def self.extended(base)
base.before do
teardown_temporary_directory
......
......@@ -4,23 +4,25 @@ describe "Pod::Platform" do
before do
@platform = Pod::Platform.new(:ios)
end
it "exposes it's symbolic name" do
@platform.name.should == :ios
end
it "can be compared for equality with another platform with the same symbolic name" do
@platform.should == Pod::Platform.new(:ios)
end
it "can be compared for equality with a matching symbolic name (backwards compatibility reasons)" do
@platform.should == :ios
end
it "uses it's name as it's string version" do
@platform.to_s.should == "ios"
it "presents an accurate string representation" do
@platform.to_s.should == "iOS"
Pod::Platform.new(:osx).to_s.should == 'OS X'
Pod::Platform.new(nil).to_s.should == "iOS - OS X"
end
it "uses it's name as it's symbold version" do
@platform.to_sym.should == :ios
end
......@@ -30,7 +32,7 @@ describe "Pod::Platform with a nil value" do
before do
@platform = Pod::Platform.new(nil)
end
it "behaves like a nil object" do
@platform.should.be.nil
end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment