lg 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Guardfile +20 -0
- data/README.md +117 -0
- data/Rakefile +1 -0
- data/bin/lg +13 -0
- data/lib/gist_store/cacert.pem +116 -0
- data/lib/gist_store/gist.rb +255 -0
- data/lib/gist_store/gist_store.rb +62 -0
- data/lib/gist_store/net_http_ext.rb +56 -0
- data/lib/logbook.rb +14 -0
- data/lib/logbook/book.rb +79 -0
- data/lib/logbook/cli.rb +127 -0
- data/lib/logbook/version.rb +3 -0
- data/logbook.gemspec +29 -0
- data/spec/gist_store/gist_store_spec.rb +86 -0
- data/spec/logbook/book_spec.rb +82 -0
- data/spec/logbook/cli_spec.rb +134 -0
- data/spec/spec_helper.rb +58 -0
- metadata +131 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'gist_store/gist'
|
2
|
+
|
3
|
+
module Logbook
|
4
|
+
class GistStore
|
5
|
+
DATE_FMT = "%Y%m%d".freeze
|
6
|
+
attr_reader :error
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@valid = !!(ENV['GITHUB_USER'] && ENV['GITHUB_PASSWORD'])
|
10
|
+
@error = @valid ? '' : "Please set up GITHUB_USER and GITHUB_PASSWORD in your env."
|
11
|
+
end
|
12
|
+
|
13
|
+
def valid?
|
14
|
+
@valid
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(id, time)
|
18
|
+
gist = Gist.read_raw(id)
|
19
|
+
return nil unless gist
|
20
|
+
file = gist['files'][gist_file(time)]
|
21
|
+
return nil unless file
|
22
|
+
file['content']
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(covertext)
|
26
|
+
u = URI.parse Gist.write(gist_data(covertext, "cover"), true)
|
27
|
+
u.path[1..-1]
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(id, time, page)
|
31
|
+
Gist.update(id, gist_data(page, gist_file(time)))
|
32
|
+
end
|
33
|
+
|
34
|
+
def all(id)
|
35
|
+
gist = Gist.read_raw(id)
|
36
|
+
return nil unless gist
|
37
|
+
|
38
|
+
entries = gist['files'].reject{|k| ["cover"].include? k }.map do |fname, v|
|
39
|
+
{ :date => DateTime.strptime(v['filename'], DATE_FMT),
|
40
|
+
:content => v['content'] }
|
41
|
+
end
|
42
|
+
|
43
|
+
{ :id => id,
|
44
|
+
:cover => gist['files']['cover']['content'],
|
45
|
+
:entries => entries }
|
46
|
+
end
|
47
|
+
|
48
|
+
def destroy(id)
|
49
|
+
Gist.delete(id)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def gist_file(time)
|
54
|
+
time.strftime DATE_FMT
|
55
|
+
end
|
56
|
+
|
57
|
+
def gist_data(text, fname)
|
58
|
+
[{:input => text, :filename => fname, :extension => "txt"}]
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
module Net
|
3
|
+
class HTTP
|
4
|
+
|
5
|
+
# Adding the patch method if it doesn't exist (rest-client issue: https://github.com/archiloque/rest-client/issues/79)
|
6
|
+
if !defined?(Net::HTTP::Patch)
|
7
|
+
# Code taken from this commit: https://github.com/ruby/ruby/commit/ab70e53ac3b5102d4ecbe8f38d4f76afad29d37d#lib/net/http.rb
|
8
|
+
class Protocol
|
9
|
+
# Sends a PATCH request to the +path+ and gets a response,
|
10
|
+
# as an HTTPResponse object.
|
11
|
+
def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
|
12
|
+
send_entity(path, data, initheader, dest, Patch, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Executes a request which uses a representation
|
16
|
+
# and returns its body.
|
17
|
+
def send_entity(path, data, initheader, dest, type, &block)
|
18
|
+
res = nil
|
19
|
+
request(type.new(path, initheader), data) {|r|
|
20
|
+
r.read_body dest, &block
|
21
|
+
res = r
|
22
|
+
}
|
23
|
+
unless @newimpl
|
24
|
+
res.value
|
25
|
+
return res, res.body
|
26
|
+
end
|
27
|
+
res
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Patch < HTTPRequest
|
32
|
+
METHOD = 'PATCH'
|
33
|
+
REQUEST_HAS_BODY = true
|
34
|
+
RESPONSE_HAS_BODY = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Replace the request method in Net::HTTP to sniff the body type
|
40
|
+
# and set the stream if appropriate
|
41
|
+
#
|
42
|
+
# Taken from:
|
43
|
+
# http://www.missiondata.com/blog/ruby/29/streaming-data-to-s3-with-ruby/
|
44
|
+
|
45
|
+
alias __request__ request
|
46
|
+
|
47
|
+
def request(req, body=nil, &block)
|
48
|
+
if body != nil && body.respond_to?(:read)
|
49
|
+
req.body_stream = body
|
50
|
+
return __request__(req, nil, &block)
|
51
|
+
else
|
52
|
+
return __request__(req, body, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/logbook.rb
ADDED
data/lib/logbook/book.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
require 'date'
|
3
|
+
require 'chronic'
|
4
|
+
require 'gist_store/gist_store'
|
5
|
+
|
6
|
+
module Logbook
|
7
|
+
class Book
|
8
|
+
attr_reader :id
|
9
|
+
attr_accessor :store
|
10
|
+
|
11
|
+
|
12
|
+
def initialize(id=nil)
|
13
|
+
@id = id
|
14
|
+
@store = Logbook.store
|
15
|
+
end
|
16
|
+
|
17
|
+
def create(covertext=nil)
|
18
|
+
store_available
|
19
|
+
|
20
|
+
cover = covertext || "#{ENV['USER'] || 'Captain'}'s log."
|
21
|
+
@id = store.create cover
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_temporal(text)
|
25
|
+
temporal = false
|
26
|
+
|
27
|
+
time = nil
|
28
|
+
if text =~ /(.+?)\s*:\s*(.*)/
|
29
|
+
time = Chronic.parse($1)
|
30
|
+
text = $2 if time
|
31
|
+
temporal = true if time
|
32
|
+
end
|
33
|
+
|
34
|
+
time ||= Time.now
|
35
|
+
|
36
|
+
add time, text
|
37
|
+
end
|
38
|
+
|
39
|
+
def add(time, text)
|
40
|
+
store_available
|
41
|
+
must_exist
|
42
|
+
|
43
|
+
page = get(time)
|
44
|
+
store.update(id, time, "#{page ? "#{page}\n" : ""}#{text}")
|
45
|
+
time
|
46
|
+
end
|
47
|
+
|
48
|
+
def get(time)
|
49
|
+
store_available
|
50
|
+
must_exist
|
51
|
+
|
52
|
+
page = store.get(id, time)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def all
|
57
|
+
store_available
|
58
|
+
must_exist
|
59
|
+
|
60
|
+
store.all(id)
|
61
|
+
end
|
62
|
+
|
63
|
+
def destroy
|
64
|
+
store_available
|
65
|
+
must_exist
|
66
|
+
|
67
|
+
store.destroy(id)
|
68
|
+
end
|
69
|
+
|
70
|
+
def store_available
|
71
|
+
raise store.error unless store.valid?
|
72
|
+
end
|
73
|
+
|
74
|
+
def must_exist
|
75
|
+
raise "create a book first" unless @id
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/lib/logbook/cli.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
|
2
|
+
require 'logbook'
|
3
|
+
require 'thor'
|
4
|
+
require 'user_config'
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
class Logbook::CLI < Thor
|
9
|
+
|
10
|
+
desc "book?", "which book are we on?"
|
11
|
+
def book?
|
12
|
+
j = current_book
|
13
|
+
return unless j
|
14
|
+
say config[:books][j.id]
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "book [ID]", "open a book"
|
18
|
+
def book(*params)
|
19
|
+
text = params.join ' '
|
20
|
+
config[:books] ||= {}
|
21
|
+
|
22
|
+
# no argument given: prompt user to switch books from a menu
|
23
|
+
if text.empty? && !config[:books].empty?
|
24
|
+
choices = config[:books].to_a
|
25
|
+
choices = choices.map.with_index{ |a, i| [i+1, *a]}
|
26
|
+
print_table choices
|
27
|
+
selection = ask("Pick one:").to_i
|
28
|
+
if selection == 0 || choices.size < selection
|
29
|
+
error "No such option"
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
selected_id = choices[selection-1][1]
|
34
|
+
self.current_book = selected_id
|
35
|
+
ok "selected"
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
# we have an argument, switch to, or create a new one.
|
40
|
+
if config[:books].has_key? text
|
41
|
+
self.current_book = text
|
42
|
+
ok "switched"
|
43
|
+
else
|
44
|
+
if yes? "Create #{em(text)}?"
|
45
|
+
j = Logbook::Book.new
|
46
|
+
begin
|
47
|
+
id = j.create text
|
48
|
+
self.current_book = id
|
49
|
+
|
50
|
+
config[:books][id] = text
|
51
|
+
config.save
|
52
|
+
ok "saved '#{text}' as #{em(id)}"
|
53
|
+
rescue
|
54
|
+
error $!
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "add MEMORY", "add a new memory"
|
61
|
+
def add(*memory)
|
62
|
+
text = memory.join ' '
|
63
|
+
j = current_book
|
64
|
+
return unless j
|
65
|
+
begin
|
66
|
+
time = j.add_temporal text
|
67
|
+
ok("at #{em(time.to_s)}")
|
68
|
+
rescue
|
69
|
+
error $!
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "all", "list the entire book"
|
74
|
+
def all
|
75
|
+
j = current_book
|
76
|
+
begin
|
77
|
+
data = j.all
|
78
|
+
say "[#{em data[:cover]}]\n"
|
79
|
+
data[:entries].group_by{|g| g[:date]}.each do |key, entries|
|
80
|
+
say "#{em key.strftime("%A, %d-%b-%Y")}"
|
81
|
+
entries.each do |entry|
|
82
|
+
say "#{entry[:content]}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
rescue
|
86
|
+
error $!
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
private
|
92
|
+
def current_book
|
93
|
+
id = config[:current_book]
|
94
|
+
unless id
|
95
|
+
error "No book is set. Create one with $ lg book #{em 'My Stories Unfold'}."
|
96
|
+
return nil
|
97
|
+
end
|
98
|
+
|
99
|
+
Logbook::Book.new id
|
100
|
+
end
|
101
|
+
|
102
|
+
def current_book=(id)
|
103
|
+
config[:current_book] = id
|
104
|
+
config.save
|
105
|
+
end
|
106
|
+
|
107
|
+
def config
|
108
|
+
@uconf ||= UserConfig.new(".logbook")
|
109
|
+
@uconf["logbook.yaml"]
|
110
|
+
end
|
111
|
+
|
112
|
+
def em(text)
|
113
|
+
shell.set_color(text, nil, true)
|
114
|
+
end
|
115
|
+
|
116
|
+
def ok(detail=nil)
|
117
|
+
text = detail ? "OK, #{detail}." : "OK."
|
118
|
+
say text, :green
|
119
|
+
end
|
120
|
+
|
121
|
+
def error(detail)
|
122
|
+
say detail, :red
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
end
|
127
|
+
|
data/logbook.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "logbook/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "lg"
|
7
|
+
s.version = Logbook::VERSION
|
8
|
+
s.authors = ["Dotan Nahum"]
|
9
|
+
s.email = ["jondotan@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{log your memories onto virtual logbooks made of Gists}
|
12
|
+
s.description = %q{log your memories onto virtual logbooks made of Gists}
|
13
|
+
|
14
|
+
s.rubyforge_project = "lg"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rr"
|
23
|
+
s.add_development_dependency "fakefs"
|
24
|
+
s.add_development_dependency "guard-minitest"
|
25
|
+
s.add_runtime_dependency "user_config"
|
26
|
+
s.add_runtime_dependency "thor"
|
27
|
+
s.add_runtime_dependency "chronic"
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'gist_store/gist_store'
|
3
|
+
|
4
|
+
def stamp
|
5
|
+
@stamp ||= DateTime.new(1981,9,8)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe Logbook::GistStore do
|
9
|
+
it "should get content" do
|
10
|
+
mock(Gist).read_raw('bid') {
|
11
|
+
{
|
12
|
+
'files' => {
|
13
|
+
'19810908' => {
|
14
|
+
'content' => "hello world"
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
g = Logbook::GistStore.new
|
21
|
+
g.get('bid', stamp).must_equal "hello world"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should create" do
|
25
|
+
mock(Gist).write(
|
26
|
+
[{
|
27
|
+
:input => "hello world",
|
28
|
+
:filename => "cover",
|
29
|
+
:extension => "txt"
|
30
|
+
}],
|
31
|
+
true
|
32
|
+
){
|
33
|
+
"http://example.com/id"
|
34
|
+
}
|
35
|
+
|
36
|
+
g = Logbook::GistStore.new
|
37
|
+
g.create("hello world")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should list all" do
|
41
|
+
mock(Gist).read_raw('bid'){
|
42
|
+
{
|
43
|
+
'files' => {
|
44
|
+
'19810908' => {
|
45
|
+
'filename' => '19810908',
|
46
|
+
'content' => "hello world"
|
47
|
+
},
|
48
|
+
'cover' => {
|
49
|
+
'filename' => 'cover',
|
50
|
+
'content' => "my book"
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
g = Logbook::GistStore.new
|
57
|
+
book = g.all('bid')
|
58
|
+
|
59
|
+
book.must_equal(
|
60
|
+
{
|
61
|
+
:id => 'bid',
|
62
|
+
:cover => 'my book',
|
63
|
+
:entries => [{:date => DateTime.strptime("19810908", "%Y%m%d"), :content => 'hello world'}]
|
64
|
+
}
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should be invalid when env variables are not set" do
|
69
|
+
ENV['GITHUB_USER'] = nil
|
70
|
+
ENV['GITHUB_PASSWORD'] = nil
|
71
|
+
|
72
|
+
g = Logbook::GistStore.new
|
73
|
+
g.valid?.must_equal false
|
74
|
+
g.error.must_match /Please set up/
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should be valid when env variables are set" do
|
78
|
+
ENV['GITHUB_USER'] = 'ghuser'
|
79
|
+
ENV['GITHUB_PASSWORD'] = 'ghpwd'
|
80
|
+
|
81
|
+
g = Logbook::GistStore.new
|
82
|
+
g.valid?.must_equal true
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|