diff --git a/bin/hub b/bin/hub index e4b0f0c694749b75378e7af8c7afc93c490cbb74..b40797c3595d22502ab25744c31bb8b6069f0f4b 100755 --- a/bin/hub +++ b/bin/hub @@ -3,291 +3,5 @@ # hub(1) # alias git=hub -class Hub - Version = '0.1.0' -end - - -class Hub - # The Args class exists to make it more convenient to work with - # command line arguments intended for git from within the Hub - # codebase. - # - # The ARGV array is converted into an Args instance by the Hub - # instance when instantiated. - class Args < Array - # With no arguments, returns the `after` callback. - # - # With a single argument, sets the `after` callback. - # Can be set to a string or a proc. - # - # If proc: - # The proc is executed after the git command is executed. For - # example, the `hub version` command sets the following proc to - # print its information after running `git version`: - # - # after { puts "hub version #{version_number}" } - # - # If string: - # The string is assumed to be a command and executed after the - # git command is executed: - # - # after "echo 'hub version #{version_number}'" - def after(command = nil, &block) - @after ||= block ? block : command - end - - # Boolean indicating whether an `after` callback has been set. - def after? - !!@after - end - end -end - - -class Hub - # The Commands module houses the git commands that hub - # lovingly wraps. If a method exists here, it is expected to have a - # corresponding git command which either gets run before or after - # the method executes. - # - # The typical flow is as follows: - # - # 1. hub is invoked from the command line: - # $ hub clone rtomayko/tilt - # - # 2. The Hub class is initialized: - # >> hub = Hub.new('clone', 'rtomayko/tilt') - # - # 3. The method representing the git subcommand is executed with the - # full args: - # >> Commands.clone('clone', 'rtomayko/tilt') - # - # 4. That method rewrites the args as it sees fit: - # >> args[1] = "git://github.com/" + args[1] + ".git" - # => "git://github.com/rtomayko/tilt.git" - # - # 5. The new args are used to run `git`: - # >> exec "git", "clone", "git://github.com/rtomayko/tilt.git" - # - # An optional `after` callback can be set. If so, it is run after - # step 5 (which then performs a `system` call rather than an - # `exec`). See `Hub::Args` for more information on the `after` callback. - module Commands - # We are a blank slate. - instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ } - extend self - - # Templates and useful information. - PRIVATE = 'git@github.com:%s/%s.git' - PUBLIC = 'git://github.com/%s/%s.git' - USER = `git config --global github.user`.chomp - REPO = `basename $(pwd)`.chomp - - # $ hub clone rtomayko/tilt - # > git clone git://github.com/rtomayko/tilt. - # - # $ hub clone -p kneath/hemingway - # > git clone git@github.com:kneath/hemingway.git - def clone(args) - ssh = args.delete('-p') - args.each_with_index do |arg, i| - if arg.scan('/').size == 1 && !arg.include?(':') - url = ssh ? PRIVATE : PUBLIC - args[i] = url % arg.split('/') - end - end - end - - # $ hub remote add pjhyett - # > git remote add pjhyett git://github.com/pjhyett/THIS_REPO.git - # - # $ hub remote add -p mojombo - # > git remote add mojombo git@github.com:mojombo/THIS_REPO.git - def remote(args) - return unless args[1] == 'add' - - # Assume GitHub usernames don't ever contain : or /, while URLs - # do. - if args[-1] !~ /:\// - ssh = args.delete('-p') - user = args.last - url = ssh ? PRIVATE : PUBLIC - args << url % [ user, REPO ] - end - end - - # $ hub init -g - # > git init - # > git remote add origin git@github.com:USER/REPO.git - def init(args) - if args.delete('-g') - url = PRIVATE % [ USER, REPO ] - args.after "git remote add origin #{url}" - end - end - - # $ hub version - # > git version - # (print hub version) - def version(args) - args.after do - puts "hub version %s" % Version - end - end - alias_method "--version", :version - - # $ hub help - # (print improved help text) - def help(args) - return if args.size > 1 - puts improved_help_text - exit - end - - # The text print when `hub help` is run, kept in its own method - # for the convenience of the author. - def improved_help_text - <<-help -usage: git [--version] [--exec-path[=GIT_EXEC_PATH]] [--html-path] - [-p|--paginate|--no-pager] [--bare] [--git-dir=GIT_DIR] - [--work-tree=GIT_WORK_TREE] [--help] COMMAND [ARGS] - -Creating a git repository: - clone Clone a repository into a new directory - init Create an empty git repository or reinitialize an existing one - -Working with content: - add Add file contents to the index - branch List, create, or delete branches - checkout Checkout a branch or paths to the working tree - commit Record changes to the repository - diff Show changes between commits, commit and working tree, etc - log Show commit logs - merge Join two or more development histories together - mv Move or rename a file, a directory, or a symlink - rm Remove files from the working tree and from the index - status Show the working tree status - show Show various types of objects - tag Create, list, delete or verify a tag object signed with GPG - -Over the network: - fetch Download objects and refs from another repository - pull Fetch from and merge with another repository or a local branch - push Update remote refs along with associated objects - remote Manage a set of tracked repositories - -Advanced commands: - bisect Find by binary search the change that introduced a bug - grep Print lines matching a pattern - reset Reset current HEAD to the specified state - rebase Forward-port local commits to the updated upstream head - -See 'git help COMMAND' for more information on a specific command. -help - end - - # All calls to `puts` in after hooks or commands are paged, - # git-style. - def puts(content) - page_stdout - super - end - - # http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby - def page_stdout - return if PLATFORM =~ /win32/ - return unless $stdout.tty? - - read, write = IO.pipe - - if Kernel.fork - # Parent process, become pager - $stdin.reopen(read) - read.close - write.close - - # Don't page if the input is short enough - ENV['LESS'] = 'FSRX' - - # Wait until we have input before we start the pager - Kernel.select [STDIN] - - pager = ENV['PAGER'] || 'less' - exec pager rescue exec "/bin/sh", "-c", pager - else - # Child process - $stdout.reopen(write) - $stderr.reopen(write) if $stderr.tty? - read.close - write.close - end - end - end -end - - -# The Hub class serves as both our namespace and controlling -# application. It expects to be initialized with `ARGV` and primarily -# exists to run a git command. -# -# The actual functionality, that is, the code it runs when it needs to -# augment a git command, is kept in the `Hub::Commands` module. -class Hub - attr_reader :args - def initialize(*args) - @args = Args.new(args) - - # Hack to emulate git-style - @args[0] = 'help' if @args.empty? - - if Commands.respond_to?(@args[0]) - Commands.send(@args[0], @args) - end - end - - # Returns the current after callback, which (if set) is run after - # the target git command. - # - # See the `Hub::Args` class for more information on the `after` - # callback. - def after - args.after.to_s - end - - # A string representation of the git command we would run if - # #execute were called. - def command - "git #{args.join(' ')}" - end - - # Runs the target git command with an optional callback. Replaces - # the current process. - def execute - if args.after? - execute_with_after_callback - else - exec "git", *args - end - end - - # Runs the target git command then executes the `after` callback. - # - # See the `Hub::Args` class for more information on the `after` - # callback. - def execute_with_after_callback - after = args.after - if system("git", *args) - after.respond_to?(:call) ? after.call : exec(after) - exit - else - exit 1 - end - end -end - -# Hack for Rubygems. -# But really, you don't want to run hub under Rubygems. -if $0.split('/').last == __FILE__.split('/').last - Hub.new(*ARGV).execute -end +require 'hub' +Hub::Runner.execute(*ARGV) diff --git a/lib/hub.rb b/lib/hub.rb new file mode 100644 index 0000000000000000000000000000000000000000..abbad6008dae18b4003ca19148726ef439edf569 --- /dev/null +++ b/lib/hub.rb @@ -0,0 +1,7 @@ +require 'hub/args' +require 'hub/commands' +require 'hub/runner' + +module Hub + Version = '0.1.0' +end diff --git a/lib/hub/args.rb b/lib/hub/args.rb new file mode 100644 index 0000000000000000000000000000000000000000..4433a5bd9217021901c398f018a62f2e615388b2 --- /dev/null +++ b/lib/hub/args.rb @@ -0,0 +1,35 @@ +module Hub + # The Args class exists to make it more convenient to work with + # command line arguments intended for git from within the Hub + # codebase. + # + # The ARGV array is converted into an Args instance by the Hub + # instance when instantiated. + class Args < Array + # With no arguments, returns the `after` callback. + # + # With a single argument, sets the `after` callback. + # Can be set to a string or a proc. + # + # If proc: + # The proc is executed after the git command is executed. For + # example, the `hub version` command sets the following proc to + # print its information after running `git version`: + # + # after { puts "hub version #{version_number}" } + # + # If string: + # The string is assumed to be a command and executed after the + # git command is executed: + # + # after "echo 'hub version #{version_number}'" + def after(command = nil, &block) + @after ||= block ? block : command + end + + # Boolean indicating whether an `after` callback has been set. + def after? + !!@after + end + end +end diff --git a/lib/hub/commands.rb b/lib/hub/commands.rb new file mode 100644 index 0000000000000000000000000000000000000000..fbd8b02f473c23435e86c4aa1575e8efc20df3d3 --- /dev/null +++ b/lib/hub/commands.rb @@ -0,0 +1,180 @@ +module Hub + # The Commands module houses the git commands that hub + # lovingly wraps. If a method exists here, it is expected to have a + # corresponding git command which either gets run before or after + # the method executes. + # + # The typical flow is as follows: + # + # 1. hub is invoked from the command line: + # $ hub clone rtomayko/tilt + # + # 2. The Hub class is initialized: + # >> hub = Hub.new('clone', 'rtomayko/tilt') + # + # 3. The method representing the git subcommand is executed with the + # full args: + # >> Commands.clone('clone', 'rtomayko/tilt') + # + # 4. That method rewrites the args as it sees fit: + # >> args[1] = "git://github.com/" + args[1] + ".git" + # => "git://github.com/rtomayko/tilt.git" + # + # 5. The new args are used to run `git`: + # >> exec "git", "clone", "git://github.com/rtomayko/tilt.git" + # + # An optional `after` callback can be set. If so, it is run after + # step 5 (which then performs a `system` call rather than an + # `exec`). See `Hub::Args` for more information on the `after` callback. + module Commands + # We are a blank slate. + instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ } + extend self + + # Templates and useful information. + PRIVATE = 'git@github.com:%s/%s.git' + PUBLIC = 'git://github.com/%s/%s.git' + USER = `git config --global github.user`.chomp + REPO = `basename $(pwd)`.chomp + + # $ hub clone rtomayko/tilt + # > git clone git://github.com/rtomayko/tilt. + # + # $ hub clone -p kneath/hemingway + # > git clone git@github.com:kneath/hemingway.git + def clone(args) + ssh = args.delete('-p') + args.each_with_index do |arg, i| + if arg.scan('/').size == 1 && !arg.include?(':') + url = ssh ? PRIVATE : PUBLIC + args[i] = url % arg.split('/') + end + end + end + + # $ hub remote add pjhyett + # > git remote add pjhyett git://github.com/pjhyett/THIS_REPO.git + # + # $ hub remote add -p mojombo + # > git remote add mojombo git@github.com:mojombo/THIS_REPO.git + def remote(args) + return unless args[1] == 'add' + + # Assume GitHub usernames don't ever contain : or /, while URLs + # do. + if args[-1] !~ /:\// + ssh = args.delete('-p') + user = args.last + url = ssh ? PRIVATE : PUBLIC + args << url % [ user, REPO ] + end + end + + # $ hub init -g + # > git init + # > git remote add origin git@github.com:USER/REPO.git + def init(args) + if args.delete('-g') + url = PRIVATE % [ USER, REPO ] + args.after "git remote add origin #{url}" + end + end + + # $ hub version + # > git version + # (print hub version) + def version(args) + args.after do + puts "hub version %s" % Version + end + end + alias_method "--version", :version + + # $ hub help + # (print improved help text) + def help(args) + return if args.size > 1 + puts improved_help_text + exit + end + + # The text print when `hub help` is run, kept in its own method + # for the convenience of the author. + def improved_help_text + <<-help +usage: git [--version] [--exec-path[=GIT_EXEC_PATH]] [--html-path] + [-p|--paginate|--no-pager] [--bare] [--git-dir=GIT_DIR] + [--work-tree=GIT_WORK_TREE] [--help] COMMAND [ARGS] + +Creating a git repository: + clone Clone a repository into a new directory + init Create an empty git repository or reinitialize an existing one + +Working with content: + add Add file contents to the index + branch List, create, or delete branches + checkout Checkout a branch or paths to the working tree + commit Record changes to the repository + diff Show changes between commits, commit and working tree, etc + log Show commit logs + merge Join two or more development histories together + mv Move or rename a file, a directory, or a symlink + rm Remove files from the working tree and from the index + status Show the working tree status + show Show various types of objects + tag Create, list, delete or verify a tag object signed with GPG + +Over the network: + fetch Download objects and refs from another repository + pull Fetch from and merge with another repository or a local branch + push Update remote refs along with associated objects + remote Manage a set of tracked repositories + +Advanced commands: + bisect Find by binary search the change that introduced a bug + grep Print lines matching a pattern + reset Reset current HEAD to the specified state + rebase Forward-port local commits to the updated upstream head + +See 'git help COMMAND' for more information on a specific command. +help + end + + # All calls to `puts` in after hooks or commands are paged, + # git-style. + def puts(content) + page_stdout + super + end + + # http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby + def page_stdout + return if PLATFORM =~ /win32/ + return unless $stdout.tty? + + read, write = IO.pipe + + if Kernel.fork + # Parent process, become pager + $stdin.reopen(read) + read.close + write.close + + # Don't page if the input is short enough + ENV['LESS'] = 'FSRX' + + # Wait until we have input before we start the pager + Kernel.select [STDIN] + + pager = ENV['PAGER'] || 'less' + exec pager rescue exec "/bin/sh", "-c", pager + else + # Child process + $stdout.reopen(write) + $stderr.reopen(write) if $stderr.tty? + read.close + write.close + end + end + end +end diff --git a/lib/hub/runner.rb b/lib/hub/runner.rb new file mode 100644 index 0000000000000000000000000000000000000000..9b5476a720b403f415f838b9a873f937f37de6a5 --- /dev/null +++ b/lib/hub/runner.rb @@ -0,0 +1,64 @@ +module Hub + # The Hub runner expects to be initialized with `ARGV` and primarily + # exists to run a git command. + # + # The actual functionality, that is, the code it runs when it needs to + # augment a git command, is kept in the `Hub::Commands` module. + class Runner + attr_reader :args + def initialize(*args) + @args = Args.new(args) + + # Hack to emulate git-style + @args[0] = 'help' if @args.empty? + + if Commands.respond_to?(@args[0]) + Commands.send(@args[0], @args) + end + end + + # Shortcut + def self.execute(*args) + new(*args).execute + end + + # Returns the current after callback, which (if set) is run after + # the target git command. + # + # See the `Hub::Args` class for more information on the `after` + # callback. + def after + args.after.to_s + end + + # A string representation of the git command we would run if + # #execute were called. + def command + "git #{args.join(' ')}" + end + + # Runs the target git command with an optional callback. Replaces + # the current process. + def execute + if args.after? + execute_with_after_callback + else + exec "git", *args + end + end + + # Runs the target git command then executes the `after` callback. + # + # See the `Hub::Args` class for more information on the `after` + # callback. + def execute_with_after_callback + after = args.after + if system("git", *args) + after.respond_to?(:call) ? after.call : exec(after) + exit + else + exit 1 + end + end + end +end diff --git a/test/hub_test.rb b/test/hub_test.rb index 8812c8c73d04b360f5db8b18c575958a5a1ba51d..c1558443908ddbb68ff1c33c0dd1ff1cff801c4a 100644 --- a/test/hub_test.rb +++ b/test/hub_test.rb @@ -1,5 +1,6 @@ require 'test/unit' -load File.dirname(__FILE__) + '/../bin/hub' +$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib' +require 'hub' class HubTest < Test::Unit::TestCase # Shortcut for creating a `Hub` instance. Pass it what you would @@ -8,7 +9,7 @@ class HubTest < Test::Unit::TestCase # shell: hub clone rtomayko/tilt # test: Hub("clone rtomayko/tilt") def Hub(args) - Hub.new(*args.split(' ')) + Hub::Runner.new(*args.split(' ')) end # Shortcut for running the `hub` command in a subprocess. Returns