Skip to content

Commit be271a8

Browse files
committed
Merge branch 'master' of https://github.com/defunkt/gist into read-gist
2 parents e4cbdd6 + d854c15 commit be271a8

File tree

6 files changed

+267
-82
lines changed

6 files changed

+267
-82
lines changed

README.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,26 @@ To read a gist and print it to STDOUT
8484

8585
## Login
8686

87-
If you want to associate your gists with your GitHub account, you need to login
88-
with gist. It doesn't store your username and password, it just uses them to get
89-
an OAuth2 token (with the "gist" permission).
87+
Before you use `gist` for the first time you will need to log in. There are two supported login flows:
88+
89+
1. The Github device-code Oauth flow. This is the default for authenticating to github.com, and can be enabled for Github Enterprise by creating an Oauth app, and exporting the environment variable `GIST_CLIENT_ID` with the client id of the Oauth app.
90+
2. The (deprecated) username and password token exchange flow. This is the default for GitHub Enterprise, and can be used to log into github.com by exporting the environment variable `GIST_USE_USERNAME_AND_PASSWORD`.
91+
92+
### The device-code flow
93+
94+
This flow allows you to obtain a token by logging into GitHub in the browser and typing a verification code. This is the preferred mechanism.
95+
96+
gist --login
97+
Requesting login parameters...
98+
Please sign in at https://github.com/login/device
99+
and enter code: XXXX-XXXX
100+
Success! https://github.com/settings/connections/applications/4f7ec0d4eab38e74384e
101+
102+
The returned access_token is stored in `~/.gist` and used for all future gisting. If you need to you can revoke access from https://github.com/settings/connections/applications/4f7ec0d4eab38e74384e.
103+
104+
### The username-password flow
105+
106+
This flow asks for your GitHub username and password (and 2FA code), and exchanges them for a token with the "gist" permission (your username and password are not stored). This mechanism is deprecated by GitHub, but may still work with GitHub Enterprise.
90107

91108
gist --login
92109
Obtaining OAuth2 access_token from GitHub.
@@ -102,8 +119,9 @@ file.
102119
#### Password-less login
103120

104121
If you have a complicated authorization requirement you can manually create a
105-
token file by pasting a GitHub token with only the `gist` permission into a
106-
file called `~/.gist`. You can create one from https://github.com/settings/tokens
122+
token file by pasting a GitHub token with `gist` scope (and maybe the `user:email`
123+
for GitHub Enterprise) into a file called `~/.gist`. You can create one from
124+
https://github.com/settings/tokens
107125

108126
This file should contain only the token (~40 hex characters), and to make it
109127
easier to edit, can optionally have a final newline (`\n` or `\r\n`).
@@ -156,7 +174,7 @@ If you need more advanced features you can also pass:
156174
* `:copy` to copy the resulting URL to the clipboard (default is false).
157175
* `:open` to open the resulting URL in a browser (default is false).
158176

159-
NOTE: The access_token must have the "gist" scope.
177+
NOTE: The access_token must have the `gist` scope and may also require the `user:email` scope.
160178

161179
‌If you want to upload multiple files in the same gist, you can:
162180

build/gist

Lines changed: 93 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,7 @@ end
13181318
module Gist
13191319
extend self
13201320

1321-
VERSION = '5.0.0'
1321+
VERSION = '6.0.0'
13221322

13231323
# A list of clipboard commands with copy and paste support.
13241324
CLIPBOARD_COMMANDS = {
@@ -1329,12 +1329,16 @@ module Gist
13291329
}
13301330

13311331
GITHUB_API_URL = URI("https://api.github.com/")
1332+
GITHUB_URL = URI("https://github.com/")
13321333
GIT_IO_URL = URI("https://git.io")
13331334

13341335
GITHUB_BASE_PATH = ""
13351336
GHE_BASE_PATH = "/api/v3"
13361337

1338+
GITHUB_CLIENT_ID = '4f7ec0d4eab38e74384e'
1339+
13371340
URL_ENV_NAME = "GITHUB_URL"
1341+
CLIENT_ID_ENV_NAME = "GIST_CLIENT_ID"
13381342

13391343
USER_AGENT = "gist/#{VERSION} (Net::HTTP, #{RUBY_DESCRIPTION})"
13401344

@@ -1350,7 +1354,7 @@ module Gist
13501354
module AuthTokenFile
13511355
def self.filename
13521356
if ENV.key?(URL_ENV_NAME)
1353-
File.expand_path "~/.gist.#{ENV[URL_ENV_NAME].gsub(/:/, '.').gsub(/[^a-z0-9.]/, '')}"
1357+
File.expand_path "~/.gist.#{ENV[URL_ENV_NAME].gsub(/:/, '.').gsub(/[^a-z0-9.-]/, '')}"
13541358
else
13551359
File.expand_path "~/.gist"
13561360
end
@@ -1416,7 +1420,7 @@ module Gist
14161420
def multi_gist(files, options={})
14171421
if options[:anonymous]
14181422
raise 'Anonymous gists are no longer supported. Please log in with `gist --login`. ' \
1419-
'(Github now requires credentials to gist https://bit.ly/2GBBxKw)'
1423+
'(GitHub now requires credentials to gist https://bit.ly/2GBBxKw)'
14201424
else
14211425
access_token = (options[:access_token] || auth_token())
14221426
end
@@ -1442,9 +1446,9 @@ module Gist
14421446

14431447
url = "#{base_path}/gists"
14441448
url << "/" << CGI.escape(existing_gist) if existing_gist.to_s != ''
1445-
url << "?access_token=" << CGI.escape(access_token) if access_token.to_s != ''
14461449

14471450
request = Net::HTTP::Post.new(url)
1451+
request['Authorization'] = "token #{access_token}" if access_token.to_s != ''
14481452
request.body = JSON.dump(json)
14491453
request.content_type = 'application/json'
14501454

@@ -1480,9 +1484,10 @@ module Gist
14801484
if user == ""
14811485
access_token = auth_token()
14821486
if access_token.to_s != ''
1483-
url << "/gists?access_token=" << CGI.escape(access_token)
1487+
url << "/gists"
14841488

14851489
request = Net::HTTP::Get.new(url)
1490+
request['Authorization'] = "token #{access_token}"
14861491
response = http(api_url, request)
14871492

14881493
pretty_gist(response)
@@ -1505,30 +1510,21 @@ module Gist
15051510
url = "#{base_path}"
15061511

15071512
if user == ""
1508-
access_token = auth_token()
1509-
if access_token.to_s != ''
1510-
url << "/gists?per_page=100&access_token=" << CGI.escape(access_token)
1511-
get_gist_pages(url)
1512-
else
1513-
raise Error, "Not authenticated. Use 'gist --login' to login or 'gist -l username' to view public gists."
1514-
end
1515-
1513+
url << "/gists?per_page=100"
15161514
else
15171515
url << "/users/#{user}/gists?per_page=100"
1518-
get_gist_pages(url)
15191516
end
15201517

1518+
get_gist_pages(url, auth_token())
15211519
end
15221520

15231521
def read_gist(id, file_name=nil)
15241522
url = "#{base_path}/gists/#{id}"
15251523

15261524
access_token = auth_token()
1527-
if access_token.to_s != ''
1528-
url << "?access_token=" << CGI.escape(access_token)
1529-
end
15301525

15311526
request = Net::HTTP::Get.new(url)
1527+
request['Authorization'] = "token #{access_token}" if access_token.to_s != ''
15321528
response = http(api_url, request)
15331529

15341530
if response.code == '200'
@@ -1554,9 +1550,8 @@ module Gist
15541550

15551551
access_token = auth_token()
15561552
if access_token.to_s != ''
1557-
url << "?access_token=" << CGI.escape(access_token)
1558-
15591553
request = Net::HTTP::Delete.new(url)
1554+
request["Authorization"] = "token #{access_token}"
15601555
response = http(api_url, request)
15611556
else
15621557
raise Error, "Not authenticated. Use 'gist --login' to login."
@@ -1569,17 +1564,18 @@ module Gist
15691564
end
15701565
end
15711566

1572-
def get_gist_pages(url)
1567+
def get_gist_pages(url, access_token = "")
15731568

15741569
request = Net::HTTP::Get.new(url)
1570+
request['Authorization'] = "token #{access_token}" if access_token.to_s != ''
15751571
response = http(api_url, request)
15761572
pretty_gist(response)
15771573

15781574
link_header = response.header['link']
15791575

15801576
if link_header
15811577
links = Hash[ link_header.gsub(/(<|>|")/, "").split(',').map { |link| link.split('; rel=') } ].invert
1582-
get_gist_pages(links['next']) if links['next']
1578+
get_gist_pages(links['next'], access_token) if links['next']
15831579
end
15841580

15851581
end
@@ -1643,16 +1639,72 @@ module Gist
16431639

16441640
# Log the user into gist.
16451641
#
1642+
def login!(credentials={})
1643+
if (login_url == GITHUB_URL || ENV.key?(CLIENT_ID_ENV_NAME)) && credentials.empty? && !ENV.key?('GIST_USE_USERNAME_AND_PASSWORD')
1644+
device_flow_login!
1645+
else
1646+
access_token_login!(credentials)
1647+
end
1648+
end
1649+
1650+
def device_flow_login!
1651+
puts "Requesting login parameters..."
1652+
request = Net::HTTP::Post.new("/login/device/code")
1653+
request.body = JSON.dump({
1654+
:scope => 'gist',
1655+
:client_id => client_id,
1656+
})
1657+
request.content_type = 'application/json'
1658+
request['accept'] = "application/json"
1659+
response = http(login_url, request)
1660+
1661+
if response.code != '200'
1662+
raise Error, "HTTP #{response.code}: #{response.body}"
1663+
end
1664+
1665+
body = JSON.parse(response.body)
1666+
1667+
puts "Please sign in at #{body['verification_uri']}"
1668+
puts " and enter code: #{body['user_code']}"
1669+
device_code = body['device_code']
1670+
interval = body['interval']
1671+
1672+
loop do
1673+
sleep(interval.to_i)
1674+
request = Net::HTTP::Post.new("/login/oauth/access_token")
1675+
request.body = JSON.dump({
1676+
:client_id => client_id,
1677+
:grant_type => 'urn:ietf:params:oauth:grant-type:device_code',
1678+
:device_code => device_code
1679+
})
1680+
request.content_type = 'application/json'
1681+
request['Accept'] = 'application/json'
1682+
response = http(login_url, request)
1683+
if response.code != '200'
1684+
raise Error, "HTTP #{response.code}: #{response.body}"
1685+
end
1686+
body = JSON.parse(response.body)
1687+
break unless body['error'] == 'authorization_pending'
1688+
end
1689+
1690+
if body['error']
1691+
raise Error, body['error_description']
1692+
end
1693+
1694+
AuthTokenFile.write JSON.parse(response.body)['access_token']
1695+
1696+
puts "Success! #{ENV[URL_ENV_NAME] || "https://github.com/"}settings/connections/applications/#{client_id}"
1697+
end
1698+
1699+
# Logs the user into gist.
1700+
#
16461701
# This method asks the user for a username and password, and tries to obtain
16471702
# and OAuth2 access token, which is then stored in ~/.gist
16481703
#
16491704
# @raise [Gist::Error] if something went wrong
1650-
# @param [Hash] credentials login details
1651-
# @option credentials [String] :username
1652-
# @option credentials [String] :password
16531705
# @see http://developer.github.com/v3/oauth/
1654-
def login!(credentials={})
1655-
puts "Obtaining OAuth2 access_token from github."
1706+
def access_token_login!(credentials={})
1707+
puts "Obtaining OAuth2 access_token from GitHub."
16561708
loop do
16571709
print "GitHub username: "
16581710
username = credentials[:username] || $stdin.gets.strip
@@ -1708,7 +1760,11 @@ module Gist
17081760
env = ENV['http_proxy'] || ENV['HTTP_PROXY']
17091761
connection = if env
17101762
proxy = URI(env)
1711-
Net::HTTP::Proxy(proxy.host, proxy.port).new(uri.host, uri.port)
1763+
if proxy.user
1764+
Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password).new(uri.host, uri.port)
1765+
else
1766+
Net::HTTP::Proxy(proxy.host, proxy.port).new(uri.host, uri.port)
1767+
end
17121768
else
17131769
Net::HTTP.new(uri.host, uri.port)
17141770
end
@@ -1862,11 +1918,19 @@ Could not find copy command, tried:
18621918
ENV.key?(URL_ENV_NAME) ? GHE_BASE_PATH : GITHUB_BASE_PATH
18631919
end
18641920

1921+
def login_url
1922+
ENV.key?(URL_ENV_NAME) ? URI(ENV[URL_ENV_NAME]) : GITHUB_URL
1923+
end
1924+
18651925
# Get the API URL
18661926
def api_url
18671927
ENV.key?(URL_ENV_NAME) ? URI(ENV[URL_ENV_NAME]) : GITHUB_API_URL
18681928
end
18691929

1930+
def client_id
1931+
ENV.key?(CLIENT_ID_ENV_NAME) ? URI(ENV[CLIENT_ID_ENV_NAME]) : GITHUB_CLIENT_ID
1932+
end
1933+
18701934
def legacy_private_gister?
18711935
return unless which('git')
18721936
`git config --global gist.private` =~ /\Ayes|1|true|on\z/i
@@ -1906,7 +1970,7 @@ filenames can be overridden by repeating the "-f" flag. The most useful reason
19061970
to do this is to change the syntax highlighting.
19071971
19081972
All gists must to be associated with a GitHub account, so you will need to login with
1909-
`gist --login` to obtain an Oauth2 access token. This is stored and used by gist in the future.
1973+
`gist --login` to obtain an OAuth2 access token. This is stored and used by gist in the future.
19101974
19111975
Private gists do not have guessable URLs and can be created with "-p", you can
19121976
also set the description at the top of the gist by passing "-d".
@@ -2023,7 +2087,7 @@ end.parse!
20232087
begin
20242088
if Gist.auth_token.nil?
20252089
puts 'Please log in with `gist --login`. ' \
2026-
'(Github now requires credentials to gist https://bit.ly/2GBBxKw)'
2090+
'(GitHub now requires credentials to gist https://bit.ly/2GBBxKw)'
20272091
exit(1)
20282092
end
20292093

0 commit comments

Comments
 (0)