gitnoted 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 514bf87ae44f1a65208940e4a8db87e5a049b27d
4
+ data.tar.gz: 944db11d0806f73287350138421c35d8b956b38f
5
+ SHA512:
6
+ metadata.gz: 0ae0007cff3bb75b96d16831d7e779ab2798a80db0dfade08fe3dcb88c9b7b6f470dcb39cb41832c9dc207f14d5a458a6e4a3e98123552ac0b9ce63ee3f2b320
7
+ data.tar.gz: 32581b4a57bcf3970965cd85f448bbd2955d3cf2b530332b2be49b7edb89a9bbff6234efc5f18abad3385ae152fbe3d5fa8c4647d1d164354ee49b4f764a64ca
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,40 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ gitnoted (0.1.0)
5
+ concurrent-ruby (>= 1.0.5)
6
+ puma (~> 3.7)
7
+ rack-cors (~> 0.4)
8
+ redcarpet (~> 3.4)
9
+ rugged (~> 0.25)
10
+ sigdump (>= 0.2.4)
11
+ sinatra (~> 1.4)
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ concurrent-ruby (1.0.5)
17
+ puma (3.8.1)
18
+ rack (1.6.5)
19
+ rack-cors (0.4.1)
20
+ rack-protection (1.5.3)
21
+ rack
22
+ rake (12.0.0)
23
+ redcarpet (3.4.0)
24
+ rugged (0.25.1.1)
25
+ sigdump (0.2.4)
26
+ sinatra (1.4.8)
27
+ rack (~> 1.5)
28
+ rack-protection (~> 1.4)
29
+ tilt (>= 1.3, < 3)
30
+ tilt (2.0.6)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ gitnoted!
37
+ rake (>= 0.9.2)
38
+
39
+ BUNDLED WITH
40
+ 1.14.6
@@ -0,0 +1,162 @@
1
+ # GitNoted
2
+
3
+ GitNoted is a simple document server that works with [Github Wiki](https://help.github.com/articles/about-github-wikis/) or [Gollum](https://github.com/gollum/gollum) to serve foot notes for external web sites just by adding a small script tag.
4
+
5
+ * [JavaScript tag](#javascript-tag)
6
+ * [Writing notes](#writing-notes)
7
+ * [Running server with Github Wiki](#running-server-with-github-wiki)
8
+ * [Running server with other git repositories](#running-server-with-other-git-repositories)
9
+ * [Deploying to Heroku](#deploying-to-heroku)
10
+ * [Server usage](#server-usage)
11
+
12
+ ## JavaScript tag
13
+
14
+ You can find the JavaScript code on [lib/git_noted/public/js/gitnoted.js](lib/git_noted/public/js/gitnoted.js). This file will be served as `/js/gitnoted.js` from the GitNoted server. You can import it using `<script>` tag as following.
15
+
16
+ ```
17
+ <!-- GitNoted depends on jQuery -->
18
+ <script src="https://code.jquery.com/jquery-3.1.1.min.js"
19
+ integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
20
+ crossorigin="anonymous"></script>
21
+
22
+ <!-- GitNoted -->
23
+ <link rel="stylesheet" type="text/css" href="http://localhost:4567/css/gitnoted.css" />
24
+ <script type="text/javascript" src="http://localhost:4567/js/gitnoted.js"></script>
25
+ ```
26
+
27
+ Please replace `http://localhost:4567` to point your GitNoted server (see bellow).
28
+
29
+ Once the JavaScript tag is set up, you can add following `<div class="gitnoted" data-labels="label,label,...">` tags your HTML to embed notes. Here is an example:
30
+
31
+ ```
32
+ <div class="gitnoted" data-labels="purpose:test,message:hello"></div>
33
+ ```
34
+
35
+ `data-labels` is used to search notes by labels. Notes that include all of them match.
36
+
37
+
38
+ ## Writing notes
39
+
40
+ Edit mode must be Markdown. Other formats are not supported.
41
+
42
+ In the body of a page, you need to include `:label: label,label,label,...` line at the beginning or at the end. Example:
43
+
44
+ ```
45
+ # My first wiki page
46
+
47
+ Hello!
48
+
49
+ ---
50
+
51
+ :label: purpose:test,message:hello
52
+ ```
53
+
54
+ ## Running server with Github Wiki
55
+
56
+ Prepare following information first:
57
+
58
+ * Domain names (and ports) of your website that embeds notes. You can have multiple web sites. Here uses `example.com` and `localhost:8080` as an example.
59
+ * Repository URL of a Github Wiki. Format of the URL is `https://github.com/<user>/<repo>.wiki.git` where "&lt;user&gt; and &lt;repo&gt; are your repository's username and repository name.
60
+
61
+ This command starts a server on http://localhost:4567:
62
+
63
+ ```
64
+ $ gem install gitnoted
65
+ $ gitnoted "https://github.com/frsyuki/gitnoted.wiki.git" \
66
+ ./repo \
67
+ -a example.com -a localhost:8080 \
68
+ -h localhost -p 4567
69
+ ```
70
+
71
+ If your repository is private, you also need to set `GITHUB_ACCESS_TOKEN` environment variable. You can create a token on [your account configuration page](https://github.com/settings/tokens). Example:
72
+
73
+ ```
74
+ $ gem install gitnoted
75
+ $ export GITHUB_ACCESS_TOKEN=abcdef0123456789abcdef0123456789abcdef01
76
+ $ gitnoted \
77
+ "https://github.com/frsyuki/my_secret_repository.wiki.git" \
78
+ ./repo \
79
+ -a example.com -a localhost:8080 \
80
+ -h localhost -p 4567
81
+ ```
82
+
83
+ ## Running server with other git repositories
84
+
85
+ Instead of using Github Wiki, you can use any git repositories that contain files named `<name>.md`. You can use your text editor or tools such as [Gollum](https://github.com/gollum/gollum) to push `.md` files.
86
+
87
+ To start GitNoted for those git repositories, prepare following information:
88
+
89
+ * Domain names (and ports) of your website that embeds notes. You can have multiple web sites. Here uses `example.com` and `localhost:8080` as an example.
90
+ * Repository URL of the git repository. It should provide http access (ssh is not supported at this moment).
91
+ * Username and password of the git repository. You need to set them to GIT_USERNAME and GIT_PASSWORD environment variables.
92
+
93
+ This command starts a server on http://localhost:4567:
94
+
95
+ ```
96
+ $ gem install gitnoted
97
+ $ export GIT_USERNAME=myname
98
+ $ export GIT_PASSWORD=topsecret
99
+ $ gitnoted \
100
+ "https://github.com/frsyuki/my_secret_repository.wiki.git" \
101
+ ./repo \
102
+ -a example.com -a localhost:8080 \
103
+ -h localhost -p 4567
104
+ ```
105
+
106
+ ## Deploying to Heroku
107
+
108
+ You need to create 4 files in a new git repository.
109
+
110
+ ### Procfile
111
+
112
+ Put a gitnoted command in your `Procfile` with `$PORT` variable as the port number. Here is an example:
113
+
114
+ ```
115
+ web: bundle exec gitnoted "https://github.com/frsyuki/gitnoted.wiki.git ./repo -a example.com -h 0.0.0.0 -p $PORT
116
+ ```
117
+
118
+ ### Gemfile
119
+
120
+ ```
121
+ source "https://rubygems.org"
122
+ ruby "2.4.0"
123
+ gem "gitnoted"
124
+ ```
125
+
126
+ ### .buildpacks
127
+
128
+ ```
129
+ https://github.com/ddollar/heroku-buildpack-apts
130
+ https://github.com/heroku/heroku-buildpack-ruby
131
+ ```
132
+
133
+ ### Aptfile
134
+
135
+ ```
136
+ cmake
137
+ pkg-config
138
+ ```
139
+
140
+ ## Server usage
141
+
142
+ ```
143
+ $ gitnoted [options] <git url> <local path to store>
144
+ options:
145
+ -a, --allow-origin DOMAIN[:PORT] Allow cross-origin resource sharing (CORS) from this domain (can be set multiple times)
146
+ -h, --host ADDRESS Bind address (default: 'localhost')
147
+ -p, --port PORT Port (default: 4567)
148
+ -e, --extra-app PATH.rb Add an extra Sinatra application
149
+ -i, --interval SECONDS Interval to update the git repository
150
+ --threads MIN:MAX Number of HTTP worker threads
151
+ environment variables:
152
+ GIT_USERNAME Git username
153
+ GIT_PASSWORD Git password
154
+ GITHUB_ACCESS_TOKEN Github personal API token
155
+ ```
156
+
157
+ ----
158
+
159
+ GitNoted
160
+ Author: Sadayuki Furuhashi
161
+ License: MIT
162
+
@@ -0,0 +1,5 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+ task :default => [:build]
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'logger'
5
+
6
+ extra_app_paths = []
7
+
8
+ scheduler_options = {
9
+ interval: 60
10
+ }
11
+
12
+ app_options = {
13
+ allow_origins: [],
14
+ }
15
+
16
+ repo_options = {
17
+ logger: Logger.new(STDOUT),
18
+ }
19
+
20
+ server_options = {
21
+ server: 'puma',
22
+ Host: 'localhost',
23
+ Port: 4567,
24
+ Threads: nil,
25
+ }
26
+
27
+ op = OptionParser.new
28
+ op.banner = "#{$0} [options] <git url> <local path to store>"
29
+
30
+ op.separator " options:"
31
+
32
+ op.on('-a', '--allow-origin DOMAIN[:PORT]', "Allow cross-origin resource sharing (CORS) from this domain (can be set multiple times)") do |v|
33
+ app_options[:allow_origins] << v
34
+ end
35
+
36
+ op.on('-h', '--host ADDRESS', "Bind address (default: 'localhost')") do |v|
37
+ server_options[:Host] = v
38
+ end
39
+
40
+ op.on('-p', '--port PORT', Integer, "Port (default: 4567)") do |v|
41
+ server_options[:Port] = v
42
+ end
43
+
44
+ op.on('-e', '--extra-app PATH.rb', "Add an extra Sinatra application") do |v|
45
+ extra_app_paths << v
46
+ end
47
+
48
+ op.on('-i', '--interval SECONDS', Integer, "Interval to update the git repository") do |v|
49
+ scheduler_options[:interval] = v
50
+ end
51
+
52
+ op.on('--threads MIN:MAX', "Number of HTTP worker threads") do |v|
53
+ server_options[:Threads] = v
54
+ end
55
+
56
+ op.separator <<EOF
57
+ environment variables:
58
+ GIT_USERNAME Git username
59
+ GIT_PASSWORD Git password
60
+ GITHUB_ACCESS_TOKEN Github personal API token
61
+ EOF
62
+
63
+
64
+ op.parse!(ARGV)
65
+
66
+ if ARGV.length != 2
67
+ puts op.to_s
68
+ exit 1
69
+ end
70
+
71
+ remote_url, local_path = *ARGV
72
+
73
+ require 'git_noted'
74
+ require 'sigdump/setup'
75
+
76
+ ENV['RACK_ENV'] ||= 'production'
77
+
78
+ if ENV['GIT_USERNAME']
79
+ repo_options[:username] = ENV['GIT_USERNAME']
80
+ repo_options[:password] = ENV['GIT_PASSWORD']
81
+ elsif ENV['GITHUB_ACCESS_TOKEN']
82
+ repo_options[:username] = ENV['GITHUB_ACCESS_TOKEN']
83
+ repo_options[:password] = 'x-oauth-basic'
84
+ end
85
+
86
+ repository = GitNoted::Repository.new(remote_url, local_path, **repo_options)
87
+ app = GitNoted::Application.with(repository: repository, **app_options)
88
+ repository.schedule_update! scheduler_options[:interval]
89
+
90
+ extra_app_paths.each do |path|
91
+ app.instance_eval(File.read(path), path)
92
+ end
93
+
94
+ app.run!(server_options)
95
+
@@ -0,0 +1,25 @@
1
+
2
+ Gem::Specification.new do |gem|
3
+ gem.name = "gitnoted"
4
+ gem.description = "Simple document server that works with Github Wiki or Gollum to serve foot notes for external websites just by adding a small script tag"
5
+ gem.homepage = "https://github.com/frsyuki/gitnoted"
6
+ gem.summary = gem.description
7
+ gem.version = "0.1.0"
8
+ gem.authors = ["Sadayuki Furuhashi"]
9
+ gem.email = ["frsyuki@gmail.com"]
10
+ gem.license = "MIT"
11
+ gem.has_rdoc = false
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15
+ gem.require_paths = ['lib']
16
+
17
+ gem.add_dependency 'sinatra', '~> 1.4'
18
+ gem.add_dependency 'puma', '~> 3.7'
19
+ gem.add_dependency 'rack-cors', '~> 0.4'
20
+ gem.add_dependency 'redcarpet', '~> 3.4'
21
+ gem.add_dependency 'rugged', '~> 0.25'
22
+ gem.add_dependency 'concurrent-ruby', '>= 1.0.5'
23
+ gem.add_dependency 'sigdump', '>= 0.2.4'
24
+ gem.add_development_dependency "rake", ">= 0.9.2"
25
+ end
@@ -0,0 +1,2 @@
1
+ require 'git_noted/application'
2
+ require 'git_noted/repository'
@@ -0,0 +1,163 @@
1
+ require 'sinatra/base'
2
+ require 'rack/cors'
3
+ require 'rack/static'
4
+ require 'redcarpet'
5
+ require 'erb'
6
+ require 'json'
7
+ require 'git_noted/repository'
8
+
9
+ module GitNoted
10
+ class Application < Sinatra::Base
11
+ def self.default_renderer
12
+ renderer = Redcarpet::Render::HTML.new({
13
+ escape_html: true,
14
+ safe_links_only: true,
15
+ })
16
+ redcarpet = Redcarpet::Markdown.new(renderer, {
17
+ tables: true,
18
+ no_intra_emphasis: true
19
+ })
20
+ redcarpet.method(:render)
21
+ end
22
+
23
+ def self.with(allow_origins: [], **options)
24
+ Class.new(self) do
25
+ alias_method :initialize_saved, :initialize
26
+ define_method(:initialize) do
27
+ initialize_saved(**options)
28
+ end
29
+
30
+ use Rack::Cors do
31
+ allow do
32
+ origins *allow_origins unless allow_origins.empty?
33
+
34
+ resource '/api/*', {
35
+ methods: [:get, :options, :head],
36
+ headers: :any,
37
+ expose: [],
38
+ credentials: true,
39
+ max_age: 600,
40
+ }
41
+ end
42
+
43
+ allow do
44
+ origins '*'
45
+ resource '/public/*', :headers => :any, :methods => :get
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def initialize(repository:, renderer: Application.default_renderer)
52
+ super()
53
+ @repository = repository
54
+ @renderer = renderer
55
+ end
56
+
57
+ attr_accessor :repository
58
+ attr_accessor :renderer
59
+
60
+ use Rack::Static, urls: ["/js", "/css"], root: File.expand_path("../public", __FILE__)
61
+
62
+ include ERB::Util
63
+
64
+ TRANSPARENT_1PX_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=".unpack('m').first
65
+
66
+ get '/' do
67
+ redirect "/index.html"
68
+ end
69
+
70
+ # GET /api/notes
71
+ # ?labels=k1:v1,k2:v2,...
72
+ # ?exclude_labels=k1:v1,k2:v2,...
73
+ get '/api/notes' do
74
+ notes = load_notes(params).map do |note|
75
+ {
76
+ labels: note.labels,
77
+ body: read_note(note)
78
+ }
79
+ end
80
+
81
+ response = {
82
+ notes: notes
83
+ }
84
+
85
+ content_type "application/json"
86
+ return response.to_json
87
+ end
88
+
89
+ # GET /api/notes.html
90
+ # ?labels=k1:v1,k2:v2,...
91
+ # ?exclude_labels=k1:v1,k2:v2,...
92
+ get '/api/notes.html' do
93
+ notes = load_notes(params)
94
+
95
+ html = %[<ul class="gitnoted">]
96
+ notes.each do |note|
97
+ body = render_note(note)
98
+ html << %[<li class="gitnoted-note">]
99
+ html << %[<div class="gitnoted-body">#{body}</div>]
100
+ html << %[<ul class="gitnoted-labels">]
101
+ note.labels.each do |label|
102
+ html << %[<li class="gitnoted-label">#{html_escape(label)}</li>]
103
+ end
104
+ html << %[</ul>]
105
+ html << %[</li>]
106
+ end
107
+ html << %[</ul>]
108
+
109
+ content_type "text/html"
110
+ return html
111
+ end
112
+
113
+ # GET /api/labels
114
+ # ?prefix=k1:
115
+ # ?used_with=k1:v1
116
+ get '/api/labels' do
117
+ labels = load_labels(params)
118
+
119
+ response = {
120
+ labels: labels
121
+ }
122
+
123
+ content_type "application/json"
124
+ return response.to_json
125
+ end
126
+
127
+ # force update
128
+ get '/api/github_hook' do
129
+ @repository.update!
130
+
131
+ response = { }
132
+
133
+ content_type "application/json"
134
+ return response.to_json
135
+ end
136
+
137
+ get '/favicon.ico' do
138
+ content_type "image/png"
139
+ return TRANSPARENT_1PX_PNG
140
+ end
141
+
142
+ def read_note(note)
143
+ @repository.read(note)
144
+ end
145
+
146
+ def render_note(note)
147
+ @renderer.call(read_note(note))
148
+ end
149
+
150
+ def load_notes(params)
151
+ label_names = (params[:labels] || '').split(",")
152
+ exclude_label_names = (params[:exclude_labels] || '').split(",")
153
+ @repository.search_notes(labels: label_names, exclude_labels: exclude_label_names)
154
+ end
155
+
156
+ def load_labels(params)
157
+ used_with_label_names = (params[:used_with] || '').split(',')
158
+ prefix = params[:prefix]
159
+ prefix = nil if prefix == ''
160
+ @repository.search_labels(prefix: prefix, used_with: used_with_label_names)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,60 @@
1
+ ul.gitnoted {
2
+ list-style-type: none;
3
+ list-style-position: outside;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ ul.gitnoted > li.gitnoted-note {
9
+ display: block;
10
+ margin: 2em 0;
11
+ padding: 0.8em 1em 0.5em 1em;
12
+ border: 1px solid #ddd;
13
+ background-color: #fff;
14
+ }
15
+
16
+ ul.gitnoted > li.gitnoted-note > div.gitnoted-body
17
+ h1 {
18
+ font-size: 120%;
19
+ }
20
+
21
+ ul.gitnoted > li.gitnoted-note > div.gitnoted-body
22
+ h2 {
23
+ font-size: 120%;
24
+ }
25
+
26
+ ul.gitnoted > li.gitnoted-note > div.gitnoted-body
27
+ h3 {
28
+ font-size: 100%;
29
+ }
30
+
31
+ ul.gitnoted > li.gitnoted-note > div.gitnoted-body
32
+ h4 {
33
+ font-size: 100%;
34
+ }
35
+
36
+ ul.gitnoted > li.gitnoted-note > ul.gitnoted-labels {
37
+ list-style-type: none;
38
+ list-style-position: outside;
39
+ text-align: right;
40
+ font-size: 70%;
41
+ }
42
+
43
+ ul.gitnoted > li.gitnoted-note > ul.gitnoted-labels > li.gitnoted-label {
44
+ display: inline-block;
45
+ margin-left: 1.3em;
46
+ margin-right: -1em;
47
+ padding: 0 0.5em;
48
+ color: #fff;
49
+ background-color: #bbb;
50
+ border-radius: 0.8em;
51
+ }
52
+
53
+ ul.gitnoted > li.gitnoted-note > ul.gitnoted-labels .gitnoted-label-button:hover {
54
+ background-color: #888;
55
+ }
56
+
57
+ ul.gitnoted > li.gitnoted-note code {
58
+ white-space: pre;
59
+ }
60
+
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>GitNotes setup completed</title>
5
+ </head>
6
+ <body>
7
+ <h1>GitNotes setup completed</h1>
8
+ <section id="api_notes_html">
9
+ <form action="/api/notes.html":>
10
+ <h2>GET /api/notes.html</h2>
11
+ <p>Search and render notes.</p>
12
+ <div>
13
+ <label for="labels">Labels (separated by comma ","):</label><br />
14
+ <input type="text" name="labels" id="labels" />
15
+ </div>
16
+ <div>
17
+ <label for="exclude_labels">Exclude labels (separated by comma ","):</label><br />
18
+ <input type="text" name="exclude_labels" id="exclude_labels" />
19
+ </div>
20
+ <input type="submit" value="Go">
21
+ </form>
22
+ </section>
23
+ <section id="api_notes">
24
+ <form action="/api/notes":>
25
+ <h2>GET /api/notes</h2>
26
+ <p>Search notes.</p>
27
+ <div>
28
+ <label for="labels">Labels (separated by comma ","):</label><br />
29
+ <input type="text" name="labels" id="labels" />
30
+ </div>
31
+ <div>
32
+ <label for="exclude_labels">Exclude labels (separated by comma ","):</label><br />
33
+ <input type="text" name="exclude_labels" id="exclude_labels" />
34
+ </div>
35
+ <input type="submit" value="Go">
36
+ </form>
37
+ </section>
38
+ <section id="api_labels">
39
+ <form action="/api/labels":>
40
+ <h2>GET /api/labels</h2>
41
+ <p>Search labels.</p>
42
+ <div>
43
+ <label for="prefix">Label name prefix:</label><br />
44
+ <input type="text" name="prefix" id="prefix" />
45
+ </div>
46
+ <div>
47
+ <label for="used_with">Used with labels (separated by comma ","):</label><br />
48
+ <input type="text" name="used_with" id="used_with" />
49
+ </div>
50
+ <input type="submit" value="Go">
51
+ </form>
52
+ </section>
53
+ </body>
54
+ </html>
@@ -0,0 +1,46 @@
1
+ var gitNotesUrl = new URL(document.currentScript.src)
2
+ gitNotesUrl.pathname = "/api/notes.html"
3
+
4
+ $(document).ready(function() {
5
+ function injectExpandScript(e, parentLabels) {
6
+ e.querySelectorAll('.gitnoted-label').forEach(function (e) {
7
+ var url = new URL(gitNotesUrl)
8
+ url.searchParams.set('labels', e.innerText)
9
+ url.searchParams.set('exclude_labels', parentLabels)
10
+ var nextParentLabels = `${parentLabels},${e.innerText}`
11
+ e.classList.add('gitnoted-label-button')
12
+ e.onclick = function() {
13
+ console.log(`exclude_labels: ${nextParentLabels}`)
14
+ fetch(url, {mode: 'cors'}).then(function (r) {
15
+ return r.text()
16
+ }).then(function (t) {
17
+ var template = document.createElement('template')
18
+ template.innerHTML = t
19
+ var note = template.content.firstChild
20
+ injectExpandScript(note, nextParentLabels)
21
+ e.parentNode.parentNode.appendChild(note, e)
22
+ e.classList.remove('gitnoted-label-button')
23
+ e.onclick = null
24
+ })
25
+ }
26
+ })
27
+ }
28
+
29
+ document.querySelectorAll('div.gitnoted').forEach(function (e) {
30
+ var url = new URL(gitNotesUrl)
31
+ if (e.dataset.labels) {
32
+ url.searchParams.set('labels', e.dataset.labels)
33
+ }
34
+ fetch(url, {mode: 'cors'}).then(function (r) {
35
+ return r.text()
36
+ }).then(function (t) {
37
+ var template = document.createElement('template')
38
+ template.innerHTML = t
39
+ var note = template.content.firstChild
40
+ if (e.dataset.labels) {
41
+ injectExpandScript(note, e.dataset.labels)
42
+ }
43
+ e.parentNode.replaceChild(note, e)
44
+ })
45
+ })
46
+ })
@@ -0,0 +1,148 @@
1
+ require 'rugged'
2
+ require 'concurrent'
3
+ require 'pathname'
4
+
5
+ module GitNoted
6
+ class Repository
7
+ class Note
8
+ def initialize(name, path, labels)
9
+ @name = name
10
+ @path = path
11
+ @labels = labels
12
+ end
13
+
14
+ attr_reader :name, :path, :labels
15
+ end
16
+
17
+ def initialize(remote_url, local_path, username: nil, password: nil, logger: Logger.new(STDERR))
18
+ @local_path = Pathname.new(local_path).cleanpath
19
+ @remote_url = remote_url
20
+ if username
21
+ @credentials = Rugged::Credentials::UserPassword.new(username: username, password: password)
22
+ else
23
+ @credentials = nil
24
+ end
25
+ @logger = logger
26
+
27
+ # initial update
28
+ update! || index!
29
+ end
30
+
31
+ def update!
32
+ begin
33
+ @repo ||= Rugged::Repository.init_at(@local_path.to_s)
34
+ updated = fetch(@repo)
35
+ return false unless updated
36
+ rescue => e
37
+ # fall back to clone.
38
+ @logger.warn "Failed to update repository incrementally. Falling back to clone: #{e}"
39
+ end
40
+ @repo = clone
41
+ index!
42
+ return true
43
+ end
44
+
45
+ def read(note)
46
+ File.read(note.path).sub(/^:label:(.*)$\n/, '')
47
+ end
48
+
49
+ def schedule_update!(interval)
50
+ Concurrent::ScheduledTask.execute(interval) do
51
+ begin
52
+ update!
53
+ rescue => e
54
+ @logger.error "Failed to update repository: #{e}"
55
+ e.backtrace.each do |bt|
56
+ @logger.error " #{bt}"
57
+ end
58
+ ensure
59
+ schedule_update!(interval)
60
+ end
61
+ end
62
+ end
63
+
64
+ def search_notes(labels: nil, exclude_labels: nil)
65
+ labels ||= []
66
+ exclude_labels ||= []
67
+
68
+ @notes.select do |note|
69
+ labels.all? {|t| note.labels.include?(t) } &&
70
+ !exclude_labels.any? {|t| note.labels.include?(t) }
71
+ end
72
+ end
73
+
74
+ def search_labels(prefix: nil, used_with: nil)
75
+ match = {}
76
+ @notes.each do |note|
77
+ if used_with.nil? || used_with.all? {|t| note.labels.include?(t) }
78
+ if prefix.nil?
79
+ matching = note.labels
80
+ else
81
+ matching = note.labels.select {|label| label.start_with?(prefix) }
82
+ end
83
+ matching.each {|label| match[label] = true }
84
+ end
85
+ end
86
+ match.keys.sort
87
+ end
88
+
89
+ private
90
+
91
+ def clone
92
+ logger_timer "Cloning remote repository." do
93
+ tmp_path = "#{@local_path}.tmp"
94
+ FileUtils.rm_rf tmp_path
95
+ Rugged::Repository.clone_at(@remote_url, tmp_path, credentials: @credentials)
96
+ FileUtils.rm_rf @local_path
97
+ FileUtils.mv tmp_path, @local_path
98
+ Rugged::Repository.init_at(@local_path.to_s)
99
+ end
100
+ end
101
+
102
+ def fetch(repo)
103
+ logger_timer "Fetching remote repository." do
104
+ remote = repo.remotes["origin"]
105
+ unless remote
106
+ raise "Remote repository is not fetched yet."
107
+ end
108
+ fetched = remote.fetch
109
+ return fetched[:total_objects] > 0
110
+ end
111
+ end
112
+
113
+ def index!
114
+ logger_timer "Updating label index." do
115
+ files = Dir["#{@local_path}/**/*.md"]
116
+ @notes = files.map do |path|
117
+ name = Pathname.new(path).relative_path_from(Pathname.new(@local_path)).sub(/\.md$/, '')
118
+ parse_note(name, path, File.read(path))
119
+ end
120
+ end
121
+ end
122
+
123
+ LABEL_KEY_CHARSET = /[a-zA-Z0-9_\-\.\/]/
124
+ LABEL_VALUE_CHARSET = /[a-zA-Z0-9_\-\.\/]/
125
+
126
+ def parse_note(name, path, text)
127
+ m = /^:label:(.*)$/.match(text)
128
+ if m
129
+ labels = m[1].scan(/[a-zA-Z0-9_\-\.\/]+:[a-zA-Z0-9_\-\.\/]+/)
130
+ else
131
+ labels = []
132
+ end
133
+
134
+ Note.new(name, path, labels)
135
+ end
136
+
137
+ def logger_timer(start_message, end_message = "%.2f seconds.")
138
+ @logger.info start_message
139
+ start_time = Time.now
140
+ begin
141
+ return yield
142
+ ensure
143
+ finish_time = Time.now
144
+ @logger.info end_message % (finish_time - start_time)
145
+ end
146
+ end
147
+ end
148
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gitnoted
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sadayuki Furuhashi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sinatra
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: puma
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack-cors
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: redcarpet
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rugged
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.25'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.25'
83
+ - !ruby/object:Gem::Dependency
84
+ name: concurrent-ruby
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 1.0.5
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 1.0.5
97
+ - !ruby/object:Gem::Dependency
98
+ name: sigdump
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 0.2.4
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 0.2.4
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: 0.9.2
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 0.9.2
125
+ description: Simple document server that works with Github Wiki or Gollum to serve
126
+ foot notes for external websites just by adding a small script tag
127
+ email:
128
+ - frsyuki@gmail.com
129
+ executables:
130
+ - gitnoted
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - Gemfile
135
+ - Gemfile.lock
136
+ - README.md
137
+ - Rakefile
138
+ - bin/gitnoted
139
+ - gitnoted.gemspec
140
+ - lib/git_noted.rb
141
+ - lib/git_noted/application.rb
142
+ - lib/git_noted/public/css/gitnoted.css
143
+ - lib/git_noted/public/index.html
144
+ - lib/git_noted/public/js/gitnoted.js
145
+ - lib/git_noted/repository.rb
146
+ homepage: https://github.com/frsyuki/gitnoted
147
+ licenses:
148
+ - MIT
149
+ metadata: {}
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ required_rubygems_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubyforge_project:
166
+ rubygems_version: 2.6.8
167
+ signing_key:
168
+ specification_version: 4
169
+ summary: Simple document server that works with Github Wiki or Gollum to serve foot
170
+ notes for external websites just by adding a small script tag
171
+ test_files: []