gitdocs 0.3.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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" }