gitdocs 0.3.6 → 0.4.0

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.
@@ -11,16 +11,28 @@ GitDocs = {
11
11
  fullPath = fullPath.replace(subpath + "/", link);
12
12
  });
13
13
  $('span.path').html(fullPath);
14
+ },
15
+ // fills in directory meta author and modified for every file
16
+ fillDirMeta : function(){
17
+ $('table.listing tbody tr').each(function(i, e) {
18
+ var file = $(e).find('a').attr('href');
19
+ $.getJSON(file + "?mode=meta", function(data) {
20
+ $(e).find('td.author').html(data.author);
21
+ $(e).find('td.modified').html(RelativeDate.time_ago_in_words(data.modified));
22
+ });
23
+ });
14
24
  }
15
25
  };
16
26
 
17
27
  $(document).ready(function() {
18
28
  GitDocs.linkBreadcrumbs();
29
+ GitDocs.fillDirMeta();
19
30
  });
20
31
 
32
+ // Redirect to edit page for new file when new file form is submitted
21
33
  $('form.add').live('submit', function(e){
22
34
  var docIdx = window.location.pathname.match(/(\d+)\//);
23
35
  var fullPath = $('span.path').text();
24
36
  window.location = "/" + docIdx[1] + fullPath + "/" + $(this).find('input.edit').val();
25
37
  e.preventDefault();
26
- });
38
+ });
@@ -14,4 +14,90 @@ Utils = {
14
14
  }
15
15
  return values;
16
16
  }
17
+ };
18
+
19
+ // DATES
20
+ // RelativeDate.time_ago_in_words(date)
21
+ var RelativeDate = {
22
+ time_ago_in_words: function(from) {
23
+ return RelativeDate.distance_of_time_in_words(new Date, RelativeDate.parseISO8601(from));
24
+
25
+ },
26
+ distance_of_time_in_words: function(to, from) {
27
+ var distance_in_seconds = ((to - from) / 1000);
28
+ var distance_in_minutes = Math.floor(distance_in_seconds / 60);
29
+
30
+ if (distance_in_minutes <= 0) { return 'less than a minute ago'; }
31
+ if (distance_in_minutes == 1) { return 'a minute ago'; }
32
+ if (distance_in_minutes < 45) { return distance_in_minutes + ' minutes ago'; }
33
+ if (distance_in_minutes < 120) { return '1 hour ago'; }
34
+ if (distance_in_minutes < 1440) { return Math.floor(distance_in_minutes / 60) + ' hours ago'; }
35
+ if (distance_in_minutes < 2880) { return '1 day ago'; }
36
+ if (distance_in_minutes < 43200) { return Math.floor(distance_in_minutes / 1440) + ' days ago'; }
37
+ if (distance_in_minutes < 86400) { return '1 month ago'; }
38
+ if (distance_in_minutes < 525960) { return Math.floor(distance_in_minutes / 43200) + ' months ago'; }
39
+ if (distance_in_minutes < 1051199) { return 'about 1 year ago'; }
40
+
41
+ return 'over ' + Math.floor(distance_in_minutes / 525960) + ' years ago';
42
+ },
43
+ parseISO8601 : function(str) {
44
+ // we assume str is a UTC date ending in 'Z'
45
+
46
+ var parts = str.split('T'),
47
+ dateParts = parts[0].split('-'),
48
+ timeParts = parts[1].split('Z'),
49
+ timeSubParts = timeParts[0].split(':'),
50
+ timeSecParts = timeSubParts[2].split('.'),
51
+ timeHours = Number(timeSubParts[0]),
52
+ _date = new Date;
53
+
54
+ _date.setUTCFullYear(Number(dateParts[0]));
55
+ _date.setUTCMonth(Number(dateParts[1])-1);
56
+ _date.setUTCDate(Number(dateParts[2]));
57
+ _date.setUTCHours(Number(timeHours));
58
+ _date.setUTCMinutes(Number(timeSubParts[1]));
59
+ _date.setUTCSeconds(Number(timeSecParts[0]));
60
+ if (timeSecParts[1]) _date.setUTCMilliseconds(Number(timeSecParts[1]));
61
+
62
+ // by using setUTC methods the date has already been converted to local time(?)
63
+ return _date;
64
+ },
65
+ humanize : function(str, shortened) {
66
+ var parts = str.split('T')[0].split('-')
67
+ var humDate = new Date;
68
+
69
+ humDate.setFullYear(Number(parts[0]));
70
+ humDate.setMonth(Number(parts[1])-1);
71
+ humDate.setDate(Number(parts[2]));
72
+
73
+ switch(humDate.getDay())
74
+ {
75
+ case 0:
76
+ var day = "Sunday";
77
+ break;
78
+ case 1:
79
+ var day = "Monday";
80
+ break;
81
+ case 2:
82
+ var day = "Tuesday";
83
+ break;
84
+ case 3:
85
+ var day = "Wednesday";
86
+ break;
87
+ case 4:
88
+ var day = "Thursday";
89
+ break;
90
+ case 5:
91
+ var day = "Friday";
92
+ break;
93
+ case 6:
94
+ var day = "Saturday";
95
+ break;
96
+ }
97
+ if(shortened) {
98
+ return humDate.toLocaleDateString();
99
+ } else {
100
+ return day + ', ' + humDate.toLocaleDateString();
101
+ }
102
+ }
17
103
  };
@@ -18,29 +18,42 @@ module Gitdocs
18
18
  @current_remote = @share.remote_name
19
19
  @current_branch = @share.branch_name
20
20
  @current_revision = sh_string("git rev-parse HEAD")
21
+ mutex = Mutex.new
21
22
 
22
23
  info("Running gitdocs!", "Running gitdocs in `#{@root}'")
23
24
 
24
- mutex = Mutex.new
25
25
  # Pull changes from remote repository
26
- Thread.new do
27
- loop do
26
+ syncer = proc do
27
+ EM.defer(proc do
28
28
  mutex.synchronize { sync_changes }
29
- sleep @polling_interval
30
- end
31
- end.abort_on_exception = true
32
-
33
- # Listen for changes in local repository
34
- @listener = FSEvent.new
35
- @listener.watch(@root) do |directories|
36
- directories.uniq!
37
- directories.delete_if {|d| d =~ /\/\.git/}
38
- unless directories.empty?
39
- mutex.synchronize { push_changes }
40
- end
29
+ end, proc do
30
+ EM.add_timer(@polling_interval) {
31
+ syncer.call
32
+ }
33
+ end)
41
34
  end
42
- at_exit { @listener.stop }
43
- @listener.run
35
+ syncer.call
36
+ # Listen for changes in local repository
37
+
38
+ EM.defer(proc{
39
+ listener = Guard::Listener.select_and_init(@root, :watch_all_modifications => true)
40
+ listener.on_change { |directories|
41
+ directories.uniq!
42
+ directories.delete_if {|d| d =~ /\/\.git/}
43
+ unless directories.empty?
44
+ EM.next_tick do
45
+ EM.defer(proc {
46
+ mutex.synchronize { push_changes }
47
+ }, proc {} )
48
+ end
49
+ end
50
+ }
51
+ listener.start
52
+ }, proc{EM.stop_reactor})
53
+ end
54
+
55
+ def clear_state
56
+ @state = nil
44
57
  end
45
58
 
46
59
  def sync_changes
@@ -115,6 +128,28 @@ module Gitdocs
115
128
  end
116
129
  end
117
130
 
131
+ IGNORED_FILES = ['.gitignore']
132
+ # dir_files("some/dir") => [<Docfile>, <Docfile>]
133
+ def dir_files(dir)
134
+ dir_path = File.expand_path(dir, @root)
135
+ files = {}
136
+ ls_files = sh_string("git ls-files").split("\n").map { |f| Docfile.new(f) }
137
+ ls_files.select { |f| f.within?(dir, @root) }.each do |f|
138
+ path = File.expand_path(f.parent, root)
139
+ files[path] ||= Docdir.new(path)
140
+ files[path].files << f unless IGNORED_FILES.include?(f.name)
141
+ end
142
+ files.keys.each { |f| files[f].parent = files[File.dirname(f)] }
143
+ files[dir_path]
144
+ end
145
+
146
+ def file_meta(file)
147
+ file = file.gsub(%r{^/}, '')
148
+ author, modified = sh_string("git log --format='%aN|%ai' -n1 #{ShellTools.escape(file)}").split("|")
149
+ modified = Time.parse(modified.sub(' ', 'T')).utc.iso8601
150
+ { :author => author, :modified => modified }
151
+ end
152
+
118
153
  def valid?
119
154
  out, status = sh_with_code "git status"
120
155
  status.success?
@@ -2,6 +2,7 @@ require 'thin'
2
2
  require 'renee'
3
3
  require 'coderay'
4
4
  require 'uri'
5
+ require 'haml'
5
6
 
6
7
  module Gitdocs
7
8
  class Server
@@ -17,10 +18,10 @@ module Gitdocs
17
18
  use Rack::Static, :urls => ['/css', '/js', '/img', '/doc'], :root => File.expand_path("../public", __FILE__)
18
19
  run Renee {
19
20
  if request.path_info == '/'
20
- render! "home", :layout => 'app', :locals => {:gds => gds}
21
+ render! "home", :layout => 'app', :locals => {:conf => manager.config, :nav_state => "home" }
21
22
  else
22
23
  path 'settings' do
23
- get.render! 'settings', :layout => 'app', :locals => {:conf => manager.config}
24
+ get.render! 'settings', :layout => 'app', :locals => {:conf => manager.config, :nav_state => "settings" }
24
25
  post do
25
26
  shares = manager.config.shares
26
27
  manager.config.global.update_attributes(request.POST['config'])
@@ -45,10 +46,11 @@ module Gitdocs
45
46
  parent = File.dirname(file_path)
46
47
  parent = '' if parent == '/'
47
48
  parent = nil if parent == '.'
48
- locals = {:idx => idx, :parent => parent, :root => gd.root, :file_path => expanded_path}
49
+ locals = {:idx => idx, :parent => parent, :root => gd.root, :file_path => expanded_path, :nav_state => nil }
49
50
  mode, mime = request.params['mode'], `file -I #{ShellTools.escape(expanded_path)}`.strip
50
- # puts "mode, mime: #{mode.inspect}, #{mime.inspect}"
51
- if mode == 'save' # Saving
51
+ if mode == 'meta' # Meta
52
+ halt 200, { 'Content-Type' => 'application/json' }, gd.file_meta(file_path).to_json
53
+ elsif mode == 'save' # Saving
52
54
  File.open(expanded_path, 'w') { |f| f.print request.params['data'] }
53
55
  redirect! "/" + idx.to_s + file_path
54
56
  elsif mode == 'upload' # Uploading
@@ -58,8 +60,8 @@ module Gitdocs
58
60
  redirect! "/" + idx.to_s + file_path + "/" + filename
59
61
  elsif !File.exist?(expanded_path) # edit for non-existent file
60
62
  render! "edit", :layout => 'app', :locals => locals.merge(:contents => "")
61
- elsif File.directory?(expanded_path)
62
- contents = Dir[File.join(gd.root, request.path_info, '*')]
63
+ elsif File.directory?(expanded_path) # list directory
64
+ contents = gd.dir_files(expanded_path)
63
65
  render! "dir", :layout => 'app', :locals => locals.merge(:contents => contents)
64
66
  elsif mode == 'delete' # delete file
65
67
  FileUtils.rm(expanded_path)
@@ -1,3 +1,3 @@
1
1
  module Gitdocs
2
- VERSION = "0.3.6"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -1,11 +1,4 @@
1
- - if parent
2
- %a{ :href => parent.empty? ? "/#{idx}" : "/#{idx}#{parent}", :class => "parent" }
3
- &#8618; Back to parent
4
- - else
5
- %a{ :href => "/", :class => "parent" }
6
- &#8618; Back to selection
7
-
8
- %h2
1
+ %h2.path
9
2
  %span.path= request.path_info.empty? ? '/' : request.path_info
10
3
  - if file
11
4
  %a{ :href => "?mode=raw" } (raw)
@@ -2,7 +2,7 @@
2
2
  %html
3
3
  %head
4
4
  %title Gitdocs #{Gitdocs::VERSION}
5
- %link{ :href => "/css/reset.css", :rel => "stylesheet" }
5
+ %link{ :href => "/css/bootstrap.css", :rel => "stylesheet"}
6
6
  %link{ :href => "/css/app.css", :rel => "stylesheet" }
7
7
  %link{ :href => "/css/tilt.css", :rel => "stylesheet" }
8
8
  %link{ :href => "/css/coderay.css", :rel => "stylesheet" }
@@ -10,14 +10,23 @@
10
10
  %script{ :src => "/js/jquery.js", :type => "text/javascript", :charset => "utf-8" }
11
11
  %script{ :src => "/js/app.js", :type => "text/javascript", :charset => "utf-8" }
12
12
  %body
13
- .nav
14
- %h1
15
- %a{:href => '/'}
16
- %img{ :src => "/img/git_logo.png", :width => 30, :height => 30}
17
- Gitdocs
18
- = Gitdocs::VERSION
19
- .container
20
- = preserve(yield)
13
+ #nav.topbar
14
+ .fill
15
+ .container
16
+ %a{:class => "brand", :href => "/"} Gitdocs
17
+ %ul(class="nav")
18
+ %li{ :class => ("active" if nav_state == "home") }
19
+ %a(href = "/") Home
20
+ %li{ :class => ("active" if nav_state == "settings") }
21
+ %a(href = "/settings") Settings
21
22
 
22
- .menu
23
- %a{:href => '/settings'} Settings
23
+ #main.container
24
+ .content
25
+ - if @title
26
+ .page-header
27
+ %h1= @title
28
+ .row
29
+ .span16= preserve(yield)
30
+
31
+ %footer
32
+ %p &copy; Gitdocs v#{Gitdocs::VERSION}
@@ -1,21 +1,31 @@
1
- %h1= root
1
+ - @title = root
2
2
 
3
3
  = partial("header", :locals => { :parent => parent, :file => false, :idx => idx })
4
4
 
5
- %table
6
- -contents.each_with_index do |f, i|
5
+ %table.condensed-table.zebra-striped.listing
6
+ %thead
7
7
  %tr
8
- %td
9
- %a{ :href => "/#{idx}#{request.path_info}/#{File.basename(f)}" }
10
- ="#{request.path_info}/#{File.basename(f)}"
8
+ %th File
9
+ %th Author
10
+ %th Last Modified
11
11
 
12
+ %tbody
13
+ - contents.items.each_with_index do |f, i|
14
+ %tr
15
+ %td
16
+ %a{ :href => "/#{idx}#{request.path_info}/#{f.name}" }
17
+ = f.name
18
+ %td.author
19
+ %td.modified
12
20
 
13
- %form.add
14
- %p Add a file in this directory
15
- %input{:type => 'text', :name => "path", :class => "edit" }
16
- %input{:type => 'submit', :value => "New file" }
17
-
18
- %form.upload{ :method => "post", :enctype => "multipart/form-data", :action => "/#{idx}#{request.path_info}?mode=upload" }
19
- %p Upload a file to this directory
20
- %input{:type => 'file', :value => "Select a file", :name => "file", :class => "uploader" }
21
- %input{:type => 'submit', :value => "Upload file"}
21
+ .row
22
+ .span6
23
+ %form.add
24
+ %p Add a file in this directory
25
+ %input{:type => 'text', :name => "path", :class => "edit" }
26
+ %input{:type => 'submit', :value => "New file", :class => "btn secondary" }
27
+ .span6
28
+ %form.upload{ :method => "post", :enctype => "multipart/form-data", :action => "/#{idx}#{request.path_info}?mode=upload" }
29
+ %p Upload a file to this directory
30
+ %input{:type => 'file', :value => "Select a file", :name => "file", :class => "uploader" }
31
+ %input{:type => 'submit', :value => "Upload file", :class => "btn secondary" }
@@ -1,12 +1,12 @@
1
- %h1= root
1
+ - @title = root
2
2
 
3
3
  = partial("header", :locals => { :parent => parent, :file => true, :idx => idx })
4
4
 
5
5
  %form{ :class => "edit", :action => "/#{idx}#{request.path_info}?mode=save", :method => "post", :style => "display:none;" }
6
6
  #editor
7
7
  %textarea{ :id => 'data', :name => "data" }= preserve contents
8
- %input{ :type => 'submit', :value => "Save" }
9
- %a{ :href => "/#{idx}#{request.path_info}" } Cancel
8
+ %input{ :type => 'submit', :value => "Save", :class => "btn primary" }
9
+ %a{ :href => "/#{idx}#{request.path_info}", :class => "btn secondary" } Cancel
10
10
  %input{ :type => 'hidden', :class => 'filename', :value => request.path_info }
11
11
 
12
12
  = partial("ace_scripts")
@@ -1,4 +1,4 @@
1
- %h1= root
1
+ - @title = root
2
2
 
3
3
  = partial("header", :locals => { :parent => parent, :file => true, :idx => idx })
4
4
 
@@ -1,6 +1,10 @@
1
+ - @title = "Pick a Share"
2
+
3
+ %p Select a share to browse:
4
+
1
5
  %table
2
- - gds.each_with_index do |gd, idx|
6
+ - conf.shares.each_with_index do |share, idx|
3
7
  %tr
4
8
  %td
5
9
  %a{ :href => "/#{idx}" }
6
- =gd.root
10
+ = share.path
@@ -1,35 +1,33 @@
1
- %h1 Settings
1
+ - @title = "Settings"
2
2
 
3
3
  %form{:method => 'POST', :action => '/settings'}
4
4
  %h2 Gitdocs
5
- %div{:id => "config", :class => "config"}
6
- %dl
7
- %dt Open browser on startup?
8
- %dd
9
- %input{:type =>'hidden', :value => '0', :name=>"config[load_browser_on_startup]"}
10
- %input{:type =>'checkbox', :value => '1', :name=>"config[load_browser_on_startup]", :checked => conf.global.load_browser_on_startup ? 'checked' : nil}
11
-
5
+ #config.field.config
6
+ %input{:type =>'hidden', :value => '0', :name=>"config[load_browser_on_startup]"}
7
+ %input{:type =>'checkbox', :value => '1', :name=>"config[load_browser_on_startup]", :checked => conf.global.load_browser_on_startup ? 'checked' : nil}
8
+ %span Open browser on startup?
12
9
 
13
10
  %h2 Shares
14
- -conf.shares.each_with_index do |share, idx|
11
+ - conf.shares.each_with_index do |share, idx|
15
12
  %div{:id => "share-#{idx}", :class => "share #{idx % 2 == 0 ? 'even' : 'odd'}"}
16
13
  %dl
17
14
  %dt Path
18
15
  %dd
19
- %input{:name=>"share[#{idx}][path]", :value => share.path}
16
+ %input{:name=>"share[#{idx}][path]", :value => share.path, :class => "path" }
20
17
  %dl
21
18
  %dt Polling interval
22
19
  %dd
23
20
  %input{:name=>"share[#{idx}][polling_interval]", :value => share.polling_interval}
24
- -if share.available_remotes
21
+
22
+ - if share.available_remotes
25
23
  %dl
26
24
  %dt Remote
27
25
  %dd
28
26
  %select{:name=>"share[#{idx}][remote_branch]"}
29
- -share.available_remotes.each do |remote|
27
+ - share.available_remotes.each do |remote|
30
28
  %option{:value => remote, :selected => remote == "#{share.remote_name}/#{share.branch_name}" ? 'selected' : nil}
31
- =remote
32
- -else
29
+ = remote
30
+ - else
33
31
 
34
32
  %dl
35
33
  %dt Remote
@@ -39,14 +37,12 @@
39
37
  %dt Branch
40
38
  %dd
41
39
  %input{:name=>"share[#{idx}][branch_name]", :value => share.branch_name}
42
- %dl
43
- %dt Notifications?
44
- %dd
45
- %input{:type =>'hidden', :value => '0', :name=>"share[#{idx}][notification]"}
46
- %input{:type =>'checkbox', :value => '1', :name=>"share[#{idx}][notification]", :checked => share.notification ? 'checked' : nil}
47
- %dl.delete
48
- %dt Delete
49
- %dd
50
- %input{:type =>'button', :value => "Delete"}
51
- %input{:type =>'hidden', :name=>"share[#{idx}][delete]"}
52
- %input{:value => 'Save', :type => 'submit'}
40
+ .notify.field
41
+ %input{:type =>'hidden', :value => '0', :name=>"share[#{idx}][notification]"}
42
+ %input{:type =>'checkbox', :value => '1', :name=>"share[#{idx}][notification]", :checked => share.notification ? 'checked' : nil}
43
+ %span Notifications?
44
+ .delete
45
+ %input{:type =>'button', :value => "Delete", :class => "btn danger"}
46
+ %input{:type =>'hidden', :name=>"share[#{idx}][delete]"}
47
+
48
+ %input{:value => 'Save', :type => 'submit', :class => "btn primary" }