gist 3.1.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ .rvmrc
2
+ Gemfile.lock
3
+
4
+ # OS X
5
+ .DS_Store
6
+ .yardoc
7
+ doc
8
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format progress
3
+ -r ./spec/spec_helper.rb
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2012 Conrad Irwin <conrad.irwin@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,149 @@
1
+ gist(1) -- upload code to https://gist.github.com
2
+ =================================================
3
+
4
+ ## Synopsis
5
+
6
+ The gist gem provides a `gist` command that you can use from your terminal to
7
+ upload content to https://gist.github.com/.
8
+
9
+ ## Installation
10
+
11
+ ‌If you have ruby installed:
12
+
13
+ gem install gist
14
+
15
+ ‌If you're using Bundler:
16
+
17
+ source :rubygems
18
+ gem 'gist'
19
+
20
+ ## Command
21
+
22
+ ‌To upload the contents of `a.rb` just:
23
+
24
+ gist a.rb
25
+
26
+ ‌Upload multiple files:
27
+
28
+ gist a b c
29
+ gist *.rb
30
+
31
+ ‌By default it reads from STDIN, and you can set a filename with `-f`.
32
+
33
+ gist -f test.rb <a.rb
34
+
35
+ ‌Alternatively, you can just paste from the clipboard:
36
+
37
+ gist -P
38
+
39
+ ‌Use `-p` to make the gist private:
40
+
41
+ gist -p a.rb
42
+
43
+ ‌Use `-d` to add a description:
44
+
45
+ gist -d "Random rbx bug" a.rb
46
+
47
+ ‌You can update existing gists with `-u`:
48
+
49
+ gist lib/gist.rb bin/gist -u 42f2c239d2eb57299408
50
+
51
+ ‌If you'd like to copy the resulting URL to your clipboard, use `-c`.
52
+
53
+ gist -c <a.rb
54
+
55
+ ‌If you'd like to copy the resulting embeddable URL to your clipboard, use `--copy-js`.
56
+
57
+ gist --copy-js <a.rb
58
+
59
+ ‌And you can just ask gist to open a browser window directly with `-o`.
60
+
61
+ gist -o <a.rb
62
+
63
+ ‌See `gist --help` for more detail.
64
+
65
+ ## Login
66
+
67
+ If you want to associate your gists with your GitHub account, you need to login
68
+ with gist. It doesn't store your username and password, it just uses them to get
69
+ an OAuth2 token (with the "gist" permission).
70
+
71
+ gist --login
72
+ Obtaining OAuth2 access_token from github.
73
+ GitHub username: ConradIrwin
74
+ GitHub password:
75
+ Success! https://github.com/settings/applications
76
+
77
+ This token is stored in `~/.gist` and used for all future gisting. If you need to
78
+ you can revoke it from https://github.com/settings/applications, or just delete the
79
+ file.
80
+
81
+ ‌After you've done this, you can still upload gists anonymously with `-a`.
82
+
83
+ gist -a a.rb
84
+
85
+ # Library
86
+
87
+ ‌You can also use Gist as a library from inside your ruby code:
88
+
89
+ Gist.gist("Look.at(:my => 'awesome').code")
90
+
91
+ If you need more advanced features you can also pass:
92
+
93
+ * `:access_token` to authenticate using OAuth2 (default is `File.read("~/.gist")).
94
+ * `:filename` to change the syntax highlighting (default is `a.rb`).
95
+ * `:public` if you want your gist to have a guessable url.
96
+ * `:description` to add a description to your gist.
97
+ * `:update` to update an existing gist (can be a URL or an id).
98
+ * `:anonymous` to submit an anonymous gist (default is false).
99
+ * `:copy` to copy the resulting URL to the clipboard (default is false).
100
+ * `:open` to open the resulting URL in a browser (default is false).
101
+
102
+ NOTE: The access_token must have the "gist" scope.
103
+
104
+ ‌If you want to upload multiple files in the same gist, you can:
105
+
106
+ Gist.multi_gist("a.rb" => "Foo.bar", "a.py" => "Foo.bar")
107
+
108
+ ‌If you'd rather use gist's builtin access_token, then you can force the user
109
+ to obtain one by calling:
110
+
111
+ Gist.login!
112
+
113
+ ‌This will take them through the process of obtaining an OAuth2 token, and storing it
114
+ in `~/.gist`, where it can later be read by `Gist.gist`
115
+
116
+ ## GitHub enterprise
117
+
118
+ ‌If you'd like `gist` to use your locally installed [GitHub Enterprise](https://enterprise.github.com/),
119
+ you need to export the `GITHUB_URL` environment variable in your `~/.bashrc`.
120
+
121
+ export GITHUB_URL=http://github.internal.example.com/
122
+
123
+ ‌Once you've done this and restarted your terminal (or run `source ~/.bashrc`), gist will
124
+ automatically use github enterprise instead of the public github.com
125
+
126
+ ## Configuration
127
+
128
+ ‌If you'd like `-o` or `-c` to be the default when you use the gist executable, add an
129
+ alias to your `~/.bashrc` (or equivalent). For example:
130
+
131
+ alias gist='gist -c'
132
+
133
+ ‌If you'd prefer gist to open a different browser, then you can export the BROWSER
134
+ environment variable:
135
+
136
+ export BROWSER=google-chrome
137
+
138
+ If clipboard or browser integration don't work on your platform, please file a bug or
139
+ (more ideally) a pull request.
140
+
141
+ If you need to use an HTTP proxy to access the internet, export the `HTTP_PROXY` or
142
+ `http_proxy` environment variable and gist will use it.
143
+
144
+ ## Meta-fu
145
+
146
+ Thanks to @defunkt and @indirect for writing and maintaining versions 1 through 3.
147
+ Thanks to @rking and @ConradIrwin for maintaining version 4.
148
+
149
+ Licensed under the MIT license. Bug-reports, and pull requests are welcome.
data/Rakefile CHANGED
@@ -1,51 +1,19 @@
1
- begin
2
- require "mg"
3
- MG.new("gist.gemspec")
4
- rescue LoadError
5
- nil
6
- end
7
-
8
- desc "Build standalone script and manpages"
9
- task :build => [ :standalone, :build_man ]
10
-
11
- desc "Build standalone script"
12
- task :standalone => :load_gist do
13
- require 'gist/standalone'
14
- Gist::Standalone.save('gist')
15
- end
16
-
17
- desc "Build gist manual"
18
- task :build_man do
19
- sh "ronn -br5 --organization=GITHUB --manual='Gist Manual' man/*.ron"
20
- end
1
+ # encoding: utf-8
2
+ #
3
+ task :default => :test
21
4
 
22
- desc "Show gist manual"
23
- task :man => :build_man do
24
- exec "man man/gist.1"
5
+ desc 'run the tests' # that's non-DRY
6
+ task :test do
7
+ sh 'rspec spec'
25
8
  end
26
9
 
27
- task :load_gist do
28
- $LOAD_PATH.unshift 'lib'
29
- require 'gist'
10
+ task :clipfailtest do
11
+ sh 'PATH=/ /usr/bin/ruby -Ilib -S bin/gist -ac < lib/gist.rb'
30
12
  end
31
13
 
32
- Rake::TaskManager.class_eval do
33
- def remove_task(task_name)
34
- @tasks.delete(task_name.to_s)
35
- end
14
+ task :man do
15
+ File.write "README.md.ron", File.read("README.md").gsub(?‌, "* ")
16
+ sh 'ronn --roff --manual="Gist manual" README.md.ron'
17
+ rm 'README.md.ron'
18
+ sh 'man ./README.1'
36
19
  end
37
-
38
- # Remove mg's install task
39
- Rake.application.remove_task(:install)
40
-
41
- desc "Install standalone script and man pages"
42
- task :install => :standalone do
43
- prefix = ENV['PREFIX'] || ENV['prefix'] || '/usr/local'
44
-
45
- FileUtils.mkdir_p "#{prefix}/bin"
46
- FileUtils.cp "gist", "#{prefix}/bin"
47
-
48
- FileUtils.mkdir_p "#{prefix}/share/man/man1"
49
- FileUtils.cp "man/gist.1", "#{prefix}/share/man/man1"
50
- end
51
-
data/bin/gist CHANGED
@@ -1,20 +1,157 @@
1
1
  #!/usr/bin/env ruby
2
- #
3
- # = gist(1)
4
- #
5
- # == USAGE
6
- # gist < file.txt
7
- # echo secret | gist -p # or --private
8
- # echo "puts :hi" | gist -t rb
9
- # gist script.py
10
- #
11
- # == INSTALL
12
- # RubyGem:
13
- # gem install gist
14
- # Old school:
15
- # curl -s http://github.com/defunkt/gist/raw/master/gist > gist &&
16
- # chmod 755 gist &&
17
- # mv gist /usr/local/bin/gist
18
-
19
- require 'gist'
20
- Gist.execute(*ARGV)
2
+
3
+ require 'optparse'
4
+ require File.expand_path('../../lib/gist', __FILE__)
5
+
6
+ # For the holdings of options.
7
+ options = {}
8
+ filenames = []
9
+
10
+ opts = OptionParser.new do |opts|
11
+ executable_name = File.split($0)[1]
12
+ opts.banner = <<-EOS
13
+ Gist (v#{Gist::VERSION}) lets you upload to https://gist.github.com/
14
+
15
+ The content to be uploaded can be passed as a list of files, if none are
16
+ specified STDIN will be read. The default filename for STDIN is "a.rb", and all
17
+ filenames can be overridden by repeating the "-f" flag. The most useful reason
18
+ to do this is to change the syntax highlighting.
19
+
20
+ If you'd like your gists to be associated with your GitHub account, so that you
21
+ can edit them and find them in future, first use `gist --login` to obtain an
22
+ Oauth2 access token. This is stored and used by gist in the future.
23
+
24
+ Private gists do not have guessable URLs and can be created with "-p", you can
25
+ also set the description at the top of the gist by passing "-d".
26
+
27
+ Anonymous gists are not associated with your GitHub account, they can be created
28
+ with "-a" even after you have used "gist --login".
29
+
30
+ If you would like to shorten the resulting gist URL, use the -s flag. This will
31
+ use GitHub's URL shortener, git.io.
32
+
33
+ To copy the resulting URL to your clipboard you can use the -c option, or to
34
+ just open it directly in your browser, use -o. Using the -e option will copy the
35
+ embeddable URL to the clipboard. You can add `alias gist='gist -c'` to your
36
+ shell's rc file to configure this behaviour by default.
37
+
38
+ Instead of creating a new gist, you can update an existing one by passing its ID
39
+ or URL with "-u". For this to work, you must be logged in, and have created the
40
+ original gist with the same GitHub account.
41
+
42
+ Usage: #{executable_name} [-o|-c|-e] [-p] [-s] [-d DESC] -a] [-u URL] [-P] [-f NAME|-t EXT]* FILE*
43
+ #{executable_name} --login
44
+
45
+ EOS
46
+
47
+ opts.on("--login", "Authenticate gist on this computer.") do
48
+ Gist.login!
49
+ exit
50
+ end
51
+
52
+ opts.on("-f", "--filename [NAME.EXTENSION]", "Sets the filename and syntax type.") do |filename|
53
+ filenames << filename
54
+ options[:filename] = filename
55
+ end
56
+
57
+ opts.on("-t", "--type [EXTENSION]", "Sets the file extension and syntax type.") do |extension|
58
+ filenames << "foo.#{extension}"
59
+ options[:filename] = "foo.#{extension}"
60
+ end
61
+
62
+ opts.on("-p", "--private", "Makes your gist private.") do
63
+ options[:private] = true
64
+ end
65
+
66
+ opts.on("--no-private") do
67
+ options[:private] = false
68
+ end
69
+
70
+ opts.on("-d", "--description DESCRIPTION", "Adds a description to your gist.") do |description|
71
+ options[:description] = description
72
+ end
73
+
74
+ opts.on("-s", "--shorten", "Shorten the gist URL using git.io.") do |shorten|
75
+ options[:shorten] = shorten
76
+ end
77
+
78
+ opts.on("-u", "--update [ URL | ID ]", "Update an existing gist.") do |update|
79
+ options[:update] = update
80
+ end
81
+
82
+ opts.on("-a", "--anonymous", "Create an anonymous gist.") do
83
+ options[:anonymous] = true
84
+ end
85
+
86
+ opts.on("-c", "--copy", "Copy the resulting URL to the clipboard") do
87
+ options[:copy] = true
88
+ end
89
+
90
+ opts.on("-e", "--embed", "Copy the embed code for the gist to the clipboard") do
91
+ options[:embed] = true
92
+ options[:copy] = true
93
+ end
94
+
95
+ opts.on("-o", "--open", "Open the resulting URL in a browser") do
96
+ options[:open] = true
97
+ end
98
+
99
+ opts.on("--no-open")
100
+
101
+ opts.on("-P", "--paste", "Paste from the clipboard to gist") do
102
+ options[:paste] = true
103
+ end
104
+
105
+ opts.on_tail("-h","--help", "Show this message.") do
106
+ puts opts
107
+ exit
108
+ end
109
+
110
+ opts.on_tail("-v", "--version", "Print the version.") do
111
+ puts "gist v#{Gist::VERSION}"
112
+ exit
113
+ end
114
+
115
+ end
116
+ opts.parse!
117
+
118
+ begin
119
+ options[:output] = if options[:embed] && options[:shorten]
120
+ raise Gist::Error, "--embed does not make sense with --shorten"
121
+ elsif options[:embed]
122
+ :javascript
123
+ elsif options[:shorten]
124
+ :short_url
125
+ else
126
+ :html_url
127
+ end
128
+
129
+ options[:public] = Gist.should_be_public?(options)
130
+
131
+ if options[:paste]
132
+ puts Gist.gist(Gist.paste, options)
133
+ else
134
+ to_read = ARGV.empty? ? ['-'] : ARGV
135
+ files = {}
136
+ to_read.zip(filenames).each do |(file, name)|
137
+ files[name || file] =
138
+ begin
139
+ if file == '-'
140
+ $stderr.puts "(type a gist. <ctrl-c> to cancel, <ctrl-d> when done)" if $stdin.tty?
141
+ STDIN.read
142
+ else
143
+ File.read(File.expand_path(file))
144
+ end
145
+ rescue => e
146
+ raise e.extend(Gist::Error)
147
+ end
148
+ end
149
+
150
+ puts Gist.multi_gist(files, options)
151
+ end
152
+ rescue Gist::Error => e
153
+ puts "Error: #{e.message}"
154
+ exit 1
155
+ rescue Interrupt
156
+ # bye!
157
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+ require './lib/gist'
3
+ Gem::Specification.new do |s|
4
+ s.name = 'gist'
5
+ s.version = Gist::VERSION
6
+ s.summary = 'Just allows you to upload gists'
7
+ s.description = 'Provides a single function (Gist.gist) that uploads a gist.'
8
+ s.homepage = 'https://github.com/defunkt/gist'
9
+ s.email = ['conrad.irwin@gmail.com', 'rkingist@sharpsaw.org']
10
+ s.authors = ['Conrad Irwin', '☈king']
11
+ s.license = 'MIT'
12
+ s.files = `git ls-files`.split("\n")
13
+ s.require_paths = ["lib"]
14
+
15
+ s.executables << 'gist'
16
+
17
+ s.add_dependency 'json'
18
+ %w(rake rspec webmock ronn).each do |gem|
19
+ s.add_development_dependency gem
20
+ end
21
+ end
@@ -1,300 +1,359 @@
1
- require 'open-uri'
2
1
  require 'net/https'
3
- require 'optparse'
4
-
5
- require 'base64'
6
-
7
- require 'gist/json' unless defined?(JSON)
8
- require 'gist/manpage' unless defined?(Gist::Manpage)
9
- require 'gist/version' unless defined?(Gist::Version)
10
-
11
- # You can use this class from other scripts with the greatest of
12
- # ease.
13
- #
14
- # >> Gist.read(gist_id)
15
- # Returns the body of gist_id as a string.
16
- #
17
- # >> Gist.write(content)
18
- # Creates a gist from the string `content`. Returns the URL of the
19
- # new gist.
20
- #
21
- # >> Gist.copy(string)
22
- # Copies string to the clipboard.
23
- #
24
- # >> Gist.browse(url)
25
- # Opens URL in your default browser.
2
+ require 'cgi'
3
+ require 'json'
4
+ require 'uri'
5
+
6
+ # It just gists.
26
7
  module Gist
27
8
  extend self
28
9
 
29
- GIST_URL = 'https://api.github.com/gists/%s'
30
- CREATE_URL = 'https://api.github.com/gists'
31
-
32
- if ENV['HTTPS_PROXY']
33
- PROXY = URI(ENV['HTTPS_PROXY'])
34
- elsif ENV['HTTP_PROXY']
35
- PROXY = URI(ENV['HTTP_PROXY'])
36
- else
37
- PROXY = nil
38
- end
39
- PROXY_HOST = PROXY ? PROXY.host : nil
40
- PROXY_PORT = PROXY ? PROXY.port : nil
41
-
42
- # Parses command line arguments and does what needs to be done.
43
- def execute(*args)
44
- private_gist = defaults["private"]
45
- gist_filename = nil
46
- gist_extension = defaults["extension"]
47
- browse_enabled = defaults["browse"]
48
- description = nil
49
-
50
- opts = OptionParser.new do |opts|
51
- opts.banner = "Usage: gist [options] [filename or stdin] [filename] ...\n" +
52
- "Filename '-' forces gist to read from stdin."
53
-
54
- opts.on('-p', '--[no-]private', 'Make the gist private') do |priv|
55
- private_gist = priv
56
- end
10
+ VERSION = '4.0.0'
57
11
 
58
- t_desc = 'Set syntax highlighting of the Gist by file extension'
59
- opts.on('-t', '--type [EXTENSION]', t_desc) do |extension|
60
- gist_extension = '.' + extension
61
- end
12
+ # A list of clipboard commands with copy and paste support.
13
+ CLIPBOARD_COMMANDS = {
14
+ 'xclip' => 'xclip -o',
15
+ 'xsel' => 'xsel -o',
16
+ 'pbcopy' => 'pbpaste',
17
+ 'putclip' => 'getclip'
18
+ }
62
19
 
63
- opts.on('-d','--description DESCRIPTION', 'Set description of the new gist') do |d|
64
- description = d
65
- end
20
+ GITHUB_API_URL = URI("https://api.github.com/")
21
+ GIT_IO_URL = URI("http://git.io")
66
22
 
67
- opts.on('-o','--[no-]open', 'Open gist in browser') do |o|
68
- browse_enabled = o
69
- end
23
+ GITHUB_BASE_PATH = ""
24
+ GHE_BASE_PATH = "/api/v3"
70
25
 
71
- opts.on('-m', '--man', 'Print manual') do
72
- Gist::Manpage.display("gist")
73
- end
26
+ URL_ENV_NAME = "GITHUB_URL"
74
27
 
75
- opts.on('-v', '--version', 'Print version') do
76
- puts Gist::Version
77
- exit
78
- end
28
+ USER_AGENT = "gist/#{VERSION} (Net::HTTP, #{RUBY_DESCRIPTION})"
79
29
 
80
- opts.on('-h', '--help', 'Display this screen') do
81
- puts opts
82
- exit
83
- end
30
+ # Exception tag for errors raised while gisting.
31
+ module Error;
32
+ def self.exception(*args)
33
+ RuntimeError.new(*args).extend(self)
84
34
  end
35
+ end
36
+ class ClipboardError < RuntimeError; include Error end
85
37
 
86
- begin
38
+ # Upload a gist to https://gist.github.com
39
+ #
40
+ # @param [String] content the code you'd like to gist
41
+ # @param [Hash] options more detailed options, see
42
+ # the documentation for {multi_gist}
43
+ #
44
+ # @see http://developer.github.com/v3/gists/
45
+ def gist(content, options = {})
46
+ filename = options[:filename] || "a.rb"
47
+ multi_gist({filename => content}, options)
48
+ end
49
+
50
+ # Upload a gist to https://gist.github.com
51
+ #
52
+ # @param [Hash] files the code you'd like to gist: filename => content
53
+ # @param [Hash] options more detailed options
54
+ #
55
+ # @option options [String] :description the description
56
+ # @option options [Boolean] :public (false) is this gist public
57
+ # @option options [Boolean] :anonymous (false) is this gist anonymous
58
+ # @option options [String] :access_token (`File.read("~/.gist")`) The OAuth2 access token.
59
+ # @option options [String] :update the URL or id of a gist to update
60
+ # @option options [Boolean] :copy (false) Copy resulting URL to clipboard, if successful.
61
+ # @option options [Boolean] :open (false) Open the resulting URL in a browser.
62
+ # @option options [Symbol] :output (:all) The type of return value you'd like:
63
+ # :html_url gives a String containing the url to the gist in a browser
64
+ # :short_url gives a String contianing a git.io url that redirects to html_url
65
+ # :javascript gives a String containing a script tag suitable for embedding the gist
66
+ # :all gives a Hash containing the parsed json response from the server
67
+ #
68
+ # @return [String, Hash] the return value as configured by options[:output]
69
+ # @raise [Gist::Error] if something went wrong
70
+ #
71
+ # @see http://developer.github.com/v3/gists/
72
+ def multi_gist(files, options={})
73
+ json = {}
87
74
 
88
- opts.parse!(args)
75
+ json[:description] = options[:description] if options[:description]
76
+ json[:public] = !!options[:public]
77
+ json[:files] = {}
89
78
 
90
- if $stdin.tty? && args[0] != '-'
91
- # Run without stdin.
79
+ files.each_pair do |(name, content)|
80
+ raise "Cannot gist empty files" if content.to_s.strip == ""
81
+ json[:files][File.basename(name)] = {:content => content}
82
+ end
83
+
84
+ existing_gist = options[:update].to_s.split("/").last
85
+ if options[:anonymous]
86
+ access_token = nil
87
+ else
88
+ access_token = (options[:access_token] || File.read(auth_token_file) rescue nil)
89
+ end
92
90
 
93
- if args.empty?
94
- # No args, print help.
95
- puts opts
96
- exit
97
- end
91
+ url = "#{base_path}/gists"
92
+ url << "/" << CGI.escape(existing_gist) if existing_gist.to_s != ''
93
+ url << "?access_token=" << CGI.escape(access_token) if access_token.to_s != ''
98
94
 
99
- files = args.inject([]) do |files, file|
100
- # Check if arg is a file. If so, grab the content.
101
- abort "Can't find #{file}" unless File.exists?(file)
95
+ request = Net::HTTP::Post.new(url)
96
+ request.body = JSON.dump(json)
97
+ request.content_type = 'application/json'
102
98
 
103
- files.push({
104
- :input => File.read(file),
105
- :filename => file,
106
- :extension => (File.extname(file) if file.include?('.'))
107
- })
108
- end
99
+ retried = false
109
100
 
101
+ begin
102
+ response = http(api_url, request)
103
+ if Net::HTTPSuccess === response
104
+ on_success(response.body, options)
110
105
  else
111
- # Read from standard input.
112
- input = $stdin.read
113
- files = [{:input => input, :extension => gist_extension}]
106
+ raise "Got #{response.class} from gist: #{response.body}"
114
107
  end
115
-
116
- url = write(files, private_gist, description)
117
- browse(url) if browse_enabled
118
- puts copy(url)
119
108
  rescue => e
120
- warn e
121
- puts opts
109
+ raise if retried
110
+ retried = true
111
+ retry
122
112
  end
123
- end
124
113
 
125
- # Create a gist on gist.github.com
126
- def write(files, private_gist = false, description = nil)
127
- url = URI.parse(CREATE_URL)
114
+ rescue => e
115
+ raise e.extend Error
116
+ end
128
117
 
129
- if PROXY_HOST
130
- proxy = Net::HTTP::Proxy(PROXY_HOST, PROXY_PORT)
131
- http = proxy.new(url.host, url.port)
118
+ # Convert long github urls into short git.io ones
119
+ #
120
+ # @param [String] url
121
+ # @return [String] shortened url, or long url if shortening fails
122
+ def shorten(url)
123
+ request = Net::HTTP::Post.new("/")
124
+ request.set_form_data(:url => url)
125
+ response = http(GIT_IO_URL, request)
126
+ case response.code
127
+ when "201"
128
+ response['Location']
132
129
  else
133
- http = Net::HTTP.new(url.host, url.port)
130
+ url
134
131
  end
132
+ end
135
133
 
136
- http.use_ssl = true
137
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
138
- http.ca_file = ca_cert
139
-
140
- req = Net::HTTP::Post.new(url.path)
141
- req.body = JSON.generate(data(files, private_gist, description))
142
-
143
- user, password = auth()
144
- if user && password
145
- req.basic_auth(user, password)
134
+ # Log the user into gist.
135
+ #
136
+ # This method asks the user for a username and password, and tries to obtain
137
+ # and OAuth2 access token, which is then stored in ~/.gist
138
+ #
139
+ # @raise [Gist::Error] if something went wrong
140
+ # @see http://developer.github.com/v3/oauth/
141
+ def login!
142
+ puts "Obtaining OAuth2 access_token from github."
143
+ print "GitHub username: "
144
+ username = $stdin.gets.strip
145
+ print "GitHub password: "
146
+ password = begin
147
+ `stty -echo` rescue nil
148
+ $stdin.gets.strip
149
+ ensure
150
+ `stty echo` rescue nil
146
151
  end
147
-
148
- response = http.start{|h| h.request(req) }
149
- case response
150
- when Net::HTTPCreated
151
- JSON.parse(response.body)['html_url']
152
+ puts ""
153
+
154
+ request = Net::HTTP::Post.new("#{base_path}/authorizations")
155
+ request.body = JSON.dump({
156
+ :scopes => [:gist],
157
+ :note => "The gist gem",
158
+ :note_url => "https://github.com/ConradIrwin/gist"
159
+ })
160
+ request.content_type = 'application/json'
161
+ request.basic_auth(username, password)
162
+
163
+ response = http(api_url, request)
164
+
165
+ if Net::HTTPCreated === response
166
+ File.open(auth_token_file, 'w') do |f|
167
+ f.write JSON.parse(response.body)['token']
168
+ end
169
+ puts "Success! #{ENV[URL_ENV_NAME] || "https://github.com/"}settings/applications"
152
170
  else
153
- puts "Creating gist failed: #{response.code} #{response.message}"
154
- exit(false)
171
+ raise "Got #{response.class} from gist: #{response.body}"
155
172
  end
173
+ rescue => e
174
+ raise e.extend Error
156
175
  end
157
176
 
158
- # Given a gist id, returns its content.
159
- def read(gist_id)
160
- data = JSON.parse(open(GIST_URL % gist_id).read)
161
- data["files"].map{|name, content| content['content'] }.join("\n\n")
177
+ # Return HTTP connection
178
+ #
179
+ # @param [URI::HTTP] The URI to which to connect
180
+ # @return [Net::HTTP]
181
+ def http_connection(uri)
182
+ env = ENV['http_proxy'] || ENV['HTTP_PROXY']
183
+ connection = if env
184
+ proxy = URI(env)
185
+ Net::HTTP::Proxy(proxy.host, proxy.port).new(uri.host, uri.port)
186
+ else
187
+ Net::HTTP.new(uri.host, uri.port)
188
+ end
189
+ if uri.scheme == "https"
190
+ connection.use_ssl = true
191
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
192
+ end
193
+ connection.open_timeout = 10
194
+ connection.read_timeout = 10
195
+ connection
162
196
  end
163
197
 
164
- # Given a url, tries to open it in your browser.
165
- # TODO: Linux
166
- def browse(url)
167
- if RUBY_PLATFORM =~ /darwin/
168
- `open #{url}`
169
- elsif RUBY_PLATFORM =~ /linux/
170
- `#{ENV['BROWSER']} #{url}`
171
- elsif ENV['OS'] == 'Windows_NT' or
172
- RUBY_PLATFORM =~ /djgpp|(cyg|ms|bcc)win|mingw|wince/i
173
- `start "" "#{url}"`
198
+ # Run an HTTP operation
199
+ #
200
+ # @param [URI::HTTP] The URI to which to connect
201
+ # @param [Net::HTTPRequest] The request to make
202
+ # @return [Net::HTTPResponse]
203
+ def http(url, request)
204
+ request['User-Agent'] = USER_AGENT
205
+
206
+ http_connection(url).start do |http|
207
+ http.request request
174
208
  end
209
+ rescue Timeout::Error
210
+ raise "Could not connect to #{api_url}"
211
+ end
212
+
213
+ # Called after an HTTP response to gist to perform post-processing.
214
+ #
215
+ # @param [String] body the text body from the github api
216
+ # @param [Hash] options more detailed options, see
217
+ # the documentation for {multi_gist}
218
+ def on_success(body, options={})
219
+ json = JSON.parse(body)
220
+
221
+ output = case options[:output]
222
+ when :javascript
223
+ %Q{<script src="#{json['html_url']}.js"></script>}
224
+ when :html_url
225
+ json['html_url']
226
+ when :short_url
227
+ shorten(json['html_url'])
228
+ else
229
+ json
230
+ end
231
+
232
+ Gist.copy(output.to_s) if options[:copy]
233
+ Gist.open(json['html_url']) if options[:open]
234
+
235
+ output
175
236
  end
176
237
 
177
- # Tries to copy passed content to the clipboard.
238
+ # Copy a string to the clipboard.
239
+ #
240
+ # @param [String] content
241
+ # @raise [Gist::Error] if no clipboard integration could be found
242
+ #
178
243
  def copy(content)
179
- cmd = case true
180
- when system("type pbcopy > /dev/null 2>&1")
181
- :pbcopy
182
- when system("type xclip > /dev/null 2>&1")
183
- :xclip
184
- when system("type putclip > /dev/null 2>&1")
185
- :putclip
186
- end
244
+ IO.popen(clipboard_command(:copy), 'r+') { |clip| clip.print content }
187
245
 
188
- if cmd
189
- IO.popen(cmd.to_s, 'r+') { |clip| clip.print content }
190
- end
246
+ unless paste == content
247
+ message = 'Copying to clipboard failed.'
191
248
 
192
- content
193
- end
249
+ if ENV["TMUX"] && clipboard_command(:copy) == 'pbcopy'
250
+ message << "\nIf you're running tmux on a mac, try http://robots.thoughtbot.com/post/19398560514/how-to-copy-and-paste-with-tmux-on-mac-os-x"
251
+ end
194
252
 
195
- private
196
- # Give an array of file information and private boolean, returns
197
- # an appropriate payload for POSTing to gist.github.com
198
- def data(files, private_gist, description)
199
- i = 0
200
- file_data = {}
201
- files.each do |file|
202
- i = i + 1
203
- filename = file[:filename] ? file[:filename] : "gistfile#{i}"
204
- file_data[filename] = {:content => file[:input]}
253
+ raise Error, message
205
254
  end
206
-
207
- data = {"files" => file_data}
208
- data.merge!({ 'description' => description }) unless description.nil?
209
- data.merge!({ 'public' => !private_gist })
210
- data
255
+ rescue Error => e
256
+ raise ClipboardError, e.message + "\nAttempted to copy: #{content}"
211
257
  end
212
258
 
213
- # Returns a basic auth string of the user's GitHub credentials if set.
214
- # http://github.com/guides/local-github-config
259
+ # Get a string from the clipboard.
215
260
  #
216
- # Returns an Array of Strings if auth is found: [user, password]
217
- # Returns nil if no auth is found.
218
- def auth
219
- user = config("github.user")
220
- password = config("github.password")
221
-
222
- token = config("github.token")
223
- if password.to_s.empty? && !token.to_s.empty?
224
- abort "Please set GITHUB_PASSWORD or github.password instead of using a token."
225
- end
261
+ # @param [String] content
262
+ # @raise [Gist::Error] if no clipboard integration could be found
263
+ def paste
264
+ `#{clipboard_command(:paste)}`
265
+ end
226
266
 
227
- if user.to_s.empty? || password.to_s.empty?
267
+ # Find command from PATH environment.
268
+ #
269
+ # @param [String] cmd command name to find
270
+ # @param [String] options PATH environment variable
271
+ # @return [String] the command found
272
+ def which(cmd, path=ENV['PATH'])
273
+ if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin|cygwin/
274
+ path.split(File::PATH_SEPARATOR).each {|dir|
275
+ f = File.join(dir, cmd+".exe")
276
+ return f if File.executable?(f) && !File.directory?(f)
277
+ }
228
278
  nil
229
279
  else
230
- [ user, password ]
280
+ return system("which #{cmd} > /dev/null 2>&1")
231
281
  end
232
282
  end
233
283
 
234
- # Returns default values based on settings in your gitconfig. See
235
- # git-config(1) for more information.
284
+ # Get the command to use for the clipboard action.
236
285
  #
237
- # Settings applicable to gist.rb are:
238
- #
239
- # gist.private - boolean
240
- # gist.extension - string
241
- def defaults
242
- extension = config("gist.extension")
243
-
244
- return {
245
- "private" => config("gist.private"),
246
- "browse" => config("gist.browse"),
247
- "extension" => extension
248
- }
286
+ # @param [Symbol] action either :copy or :paste
287
+ # @return [String] the command to run
288
+ # @raise [Gist::ClipboardError] if no clipboard integration could be found
289
+ def clipboard_command(action)
290
+ command = CLIPBOARD_COMMANDS.keys.detect do |cmd|
291
+ which cmd
292
+ end
293
+ raise ClipboardError, <<-EOT unless command
294
+ Could not find copy command, tried:
295
+ #{CLIPBOARD_COMMANDS.values.join(' || ')}
296
+ EOT
297
+ action == :copy ? command : CLIPBOARD_COMMANDS[command]
249
298
  end
250
299
 
251
- # Reads a config value using:
252
- # => Environment: GITHUB_PASSWORD, GITHUB_USER
253
- # like vim gist plugin
254
- # => git-config(1)
300
+ # Open a URL in a browser.
255
301
  #
256
- # return something useful or nil
257
- def config(key)
258
- env_key = ENV[key.upcase.gsub(/\./, '_')]
259
- return env_key if env_key and not env_key.strip.empty?
302
+ # @param [String] url
303
+ # @raise [RuntimeError] if no browser integration could be found
304
+ #
305
+ # This method was heavily inspired by defunkt's Gist#open,
306
+ # @see https://github.com/defunkt/gist/blob/bca9b29/lib/gist.rb#L157
307
+ def open(url)
308
+ command = if ENV['BROWSER']
309
+ ENV['BROWSER']
310
+ elsif RUBY_PLATFORM =~ /darwin/
311
+ 'open'
312
+ elsif RUBY_PLATFORM =~ /linux/
313
+ %w(
314
+ sensible-browser
315
+ firefox
316
+ firefox-bin
317
+ ).detect do |cmd|
318
+ which cmd
319
+ end
320
+ elsif ENV['OS'] == 'Windows_NT' || RUBY_PLATFORM =~ /djgpp|(cyg|ms|bcc)win|mingw|wince/i
321
+ 'start ""'
322
+ else
323
+ raise "Could not work out how to use a browser."
324
+ end
325
+
326
+ `#{command} #{url}`
327
+ end
260
328
 
261
- str_to_bool `git config --global #{key}`.strip
329
+ # Get the API base path
330
+ def base_path
331
+ ENV.key?(URL_ENV_NAME) ? GHE_BASE_PATH : GITHUB_BASE_PATH
262
332
  end
263
333
 
264
- # Parses a value that might appear in a .gitconfig file into
265
- # something useful in a Ruby script.
266
- def str_to_bool(str)
267
- if str.size > 0 and str[0].chr == '!'
268
- command = str[1, str.length]
269
- value = `#{command}`
270
- else
271
- value = str
272
- end
334
+ # Get the API URL
335
+ def api_url
336
+ ENV.key?(URL_ENV_NAME) ? URI(ENV[URL_ENV_NAME]) : GITHUB_API_URL
337
+ end
273
338
 
274
- case value.downcase.strip
275
- when "false", "0", "nil", "", "no", "off"
276
- nil
277
- when "true", "1", "yes", "on"
278
- true
339
+ def auth_token_file
340
+ if ENV.key?(URL_ENV_NAME)
341
+ File.expand_path "~/.gist.#{ENV[URL_ENV_NAME].gsub(/[^a-z.]/, '')}"
279
342
  else
280
- value
343
+ File.expand_path "~/.gist"
281
344
  end
282
345
  end
283
346
 
284
- def ca_cert
285
- cert_file = [
286
- File.expand_path("../gist/cacert.pem", __FILE__),
287
- "/tmp/gist_cacert.pem"
288
- ].find{|l| File.exist?(l) }
347
+ def legacy_private_gister?
348
+ return unless which('git')
349
+ `git config --global gist.private` =~ /\Ayes|1|true|on\z/i
350
+ end
289
351
 
290
- if cert_file
291
- cert_file
352
+ def should_be_public?(options={})
353
+ if options.key? :private
354
+ !options[:private]
292
355
  else
293
- File.open("/tmp/gist_cacert.pem", "w") do |f|
294
- f.write(DATA.read.split("__CACERT__").last)
295
- end
296
- "/tmp/gist_cacert.pem"
356
+ !Gist.legacy_private_gister?
297
357
  end
298
358
  end
299
-
300
359
  end