gitdocs 0.5.0.pre1 → 0.5.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MThmNzE5OWRhMDk2OThhMTdlMmU2MWQ2YzhmY2RkZjUwODM5ZmI0NQ==
4
+ ZWNmOThmZjU3ZTIzNGE5ODQ3ZmM3YjEzODQ0NTNiZTgyODk5ZmQ4Ng==
5
5
  data.tar.gz: !binary |-
6
- NTk2ZDg1ODhiMGNhYzI4NTJjYmU2NWU4ZDM0MDBhMzE4NjQzYWI2Mg==
6
+ MjBjMWEzNzQ5NTA0Y2Q5MzA0M2Q3MTlhMzE1YWE2OTViZWU1YTZhNg==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- MmY3NzdkYjQ1NzA0MjA0NGY4ZmRkZWE2NGQ2YWM2MTYzNDg0ZjdmYWU4MzJh
10
- ODc5YmEyMDI0ZGE2YjY1ZDk4ZDI0MDAzZDg0MWU1YjNiMTAwOTZhOWE5NjNj
11
- NWRiNDM4YjVkNTBlOGI2ZGVhZTU4MTFhNTg3MjA5NjZjMWVjZDA=
9
+ ZjU3NDJjNTQ3YTU4OGE1OTQzNDU5ZmYzMWRiZjU4MGE5MDY3NjQ5ZGIzOTNl
10
+ YjA3MDYzNjNkNTBlNmQ2OTcwYzg1ZDRlMTA2ODUwYmU2ZGVhMzM4Mzg2NGFm
11
+ YmNlMWJkYWY0NzEzZTgxOThmMWM4NTdmOTEyOGZhMTc5MDdlMzM=
12
12
  data.tar.gz: !binary |-
13
- NDhjOGYxOGMwMzUxMDg5ZWQwZWQwNmVhYWIyOTBiNGZmNjFhNzI1ZGE0Mjkz
14
- YjVhODlmNDRlZDIwM2Q4YjJiYWZkYTFkODNmY2IwM2NjOWVjMDYyZjRmOWFh
15
- ZWYyMjdlOTBmM2RhNTY2MmQyZjQ4Y2ZhYWViY2JiZDEwNzcwMzM=
13
+ MjExM2Y3OGU1NDg0ODdhYzA1NDY5YWIxNTU0ZmRkMjY2MDNkZWI2MTk1NzFl
14
+ ZjVhZWRkYTYxY2MxZDRjMjhmMzU3OTRlMzhjZjkyNDQzYzI3M2JiMDYwNTgy
15
+ MDdhODZjOTEwODEzOTRmYzc4YTExZDZjYjJjZmQwNDRmNDMxZDc=
data/CHANGELOG CHANGED
@@ -1,7 +1,11 @@
1
+ 0.5.0.pre2 (3/9/2014)
2
+
3
+ * Convert to use rugged and Grit (@acant)
4
+
1
5
  0.5.0.pre1 (11/25/2013)
2
6
 
3
- * Upgrade thin gem to v1.5.1 (Thanks @acant)
4
- * Add TravisCI configuration (Thanks @acant)
7
+ * Upgrade thin gem to v1.5.1 (@acant)
8
+ * Add TravisCI configuration (@acant)
5
9
  * Rescue StandardError and better error notifications to fix crashes (@acant)
6
10
  * Reduce unexpected exists caused by repository and file system errors
7
11
  * Add notification of unexpected daemon exist
data/README.md CHANGED
@@ -176,6 +176,7 @@ We also have had several contributors:
176
176
  * [Chris Kempson](https://github.com/ChrisKempson) - Encoding issues
177
177
  * [Evan Tatarka](https://github.com/evant) - Front-end style fixes
178
178
  * [Kale Worsley](https://github.com/kaleworsley) - Custom commit msgs, revert revisions, front-end cleanup
179
+ * [Andrew Sullivan Cant](https://github.com/acant) - Major improvements, grit support, core contributor
179
180
 
180
181
  Gitdocs is still a young project with a lot of opportunity for contributions. Patches welcome!
181
182
 
data/gitdocs.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
23
23
  s.add_dependency 'joshbuddy-guard', '~> 0.10.0'
24
24
  s.add_dependency 'thin', '~> 1.5.1'
25
25
  s.add_dependency 'renee', '~> 0.3.11'
26
- s.add_dependency 'redcarpet', '~> 2.0.0'
26
+ s.add_dependency 'redcarpet', '~> 3.1.1'
27
27
  s.add_dependency 'thor', '~> 0.14.6'
28
28
  s.add_dependency 'coderay', '~> 1.0.4'
29
29
  s.add_dependency 'dante', '~> 0.1.2'
@@ -37,10 +37,12 @@ Gem::Specification.new do |s|
37
37
  s.add_dependency 'mimetype-fu', "~> 0.1.2"
38
38
  s.add_dependency 'eventmachine', '>= 1.0.3'
39
39
  s.add_dependency 'launchy', '~> 2.4.2'
40
+ s.add_dependency 'rugged', '~> 0.19.0'
40
41
 
41
42
  s.add_development_dependency 'minitest', "~> 5.0.8"
42
43
  s.add_development_dependency 'rake'
43
44
  s.add_development_dependency 'mocha'
44
45
  s.add_development_dependency 'fakeweb'
45
46
  s.add_development_dependency 'metric_fu'
47
+ s.add_development_dependency 'aruba'
46
48
  end
data/lib/gitdocs.rb CHANGED
@@ -4,6 +4,8 @@ require 'dante'
4
4
  require 'socket'
5
5
  require 'shell_tools'
6
6
  require 'guard'
7
+ require 'grit'
8
+ require 'rugged'
7
9
 
8
10
  require 'gitdocs/version'
9
11
  require 'gitdocs/configuration'
@@ -13,6 +15,8 @@ require 'gitdocs/cli'
13
15
  require 'gitdocs/manager'
14
16
  require 'gitdocs/docfile'
15
17
  require 'gitdocs/rendering'
18
+ require 'gitdocs/notifier'
19
+ require 'gitdocs/repository'
16
20
 
17
21
  module Gitdocs
18
22
  DEBUG = ENV['DEBUG']
data/lib/gitdocs/cli.rb CHANGED
@@ -9,6 +9,7 @@ module Gitdocs
9
9
  desc 'start', 'Starts a daemonized gitdocs process'
10
10
  method_option :debug, type: :boolean, aliases: '-D'
11
11
  method_option :port, type: :string, aliases: '-p'
12
+ method_option :pid, type: :string, aliases: '-P'
12
13
  def start
13
14
  unless stopped?
14
15
  say 'Gitdocs is already running, please use restart', :red
@@ -28,6 +29,7 @@ module Gitdocs
28
29
  end
29
30
  end
30
31
 
32
+ method_option :pid, type: :string, aliases: '-P'
31
33
  desc 'stop', 'Stops the gitdocs process'
32
34
  def stop
33
35
  unless running?
@@ -39,12 +41,14 @@ module Gitdocs
39
41
  say 'Stopped gitdocs', :red
40
42
  end
41
43
 
44
+ method_option :pid, type: :string, aliases: '-P'
42
45
  desc 'restart', 'Restarts the gitdocs process'
43
46
  def restart
44
47
  stop
45
48
  start
46
49
  end
47
50
 
51
+ method_option :pid, type: :string, aliases: '-P'
48
52
  desc 'add PATH', 'Adds a path to gitdocs'
49
53
  def add(path)
50
54
  config.add_path(path)
@@ -52,6 +56,7 @@ module Gitdocs
52
56
  restart if running?
53
57
  end
54
58
 
59
+ method_option :pid, type: :string, aliases: '-P'
55
60
  desc 'rm PATH', 'Removes a path from gitdocs'
56
61
  def rm(path)
57
62
  config.remove_path(path)
@@ -65,14 +70,15 @@ module Gitdocs
65
70
  say 'Cleared paths from gitdocs'
66
71
  end
67
72
 
73
+ method_option :pid, type: :string, aliases: '-P'
68
74
  desc 'create PATH REMOTE', 'Creates a new gitdoc root based on an existing remote'
69
75
  def create(path, remote)
70
- FileUtils.mkdir_p(File.dirname(path))
71
- system("git clone -q #{remote} #{ShellTools.escape(path)}") || fail("Unable to clone into #{path}")
76
+ Gitdocs::Repository.clone(path, remote)
72
77
  add(path)
73
78
  say "Created #{path} path for gitdoc"
74
79
  end
75
80
 
81
+ method_option :pid, type: :string, aliases: '-P'
76
82
  desc 'status', 'Retrieve gitdocs status'
77
83
  def status
78
84
  say "GitDoc v#{VERSION}"
@@ -95,10 +101,10 @@ module Gitdocs
95
101
  Launchy.open("http://localhost:#{web_port}/")
96
102
  end
97
103
 
98
- desc 'config', 'Configuration options for gitdocs'
99
- def config
100
- # TODO: make this work
101
- end
104
+ # TODO: make this work
105
+ #desc 'config', 'Configuration options for gitdocs'
106
+ #def config
107
+ #end
102
108
 
103
109
  desc 'help', 'Prints out the help'
104
110
  def help(task = nil, subcommand = false)
@@ -113,7 +119,7 @@ module Gitdocs
113
119
  'gitdocs',
114
120
  debug: false,
115
121
  daemonize: true,
116
- pid_path: pid_path
122
+ pid_path: pid_path
117
123
  )
118
124
  end
119
125
 
@@ -130,25 +136,20 @@ module Gitdocs
130
136
  end
131
137
 
132
138
  def pid_path
133
- '/tmp/gitdocs.pid'
139
+ options[:pid] || '/tmp/gitdocs.pid'
134
140
  end
135
141
 
136
142
  # @return [Symbol] to indicate how the file system is being watched
137
143
  def file_system_watch_method
138
- if Guard::Listener.mac?
139
- begin
140
- return :notification if Guard::Listener::Darwin.usable?
141
- rescue NameError ; end
142
- elsif Guard::Listener.linux?
143
- begin
144
- return :notification if Guard::Listener::Linux.usable?
145
- rescue NameError ; end
146
- elsif Guard::Listener.windows?
147
- begin
148
- return :notification if Guard::Listener::Windows.usable?
149
- rescue NameError ; end
144
+ if Guard::Listener.mac? && Guard::Darwin.usable?
145
+ :notification
146
+ elsif Guard::Listener.linux? && Guard::Linux.usable?
147
+ :notification
148
+ elsif Guard::Listener.windows? && Guard::Windows.usable?
149
+ :notification
150
+ else
151
+ :polling
150
152
  end
151
- :polling
152
153
  end
153
154
  end
154
155
  end
@@ -18,20 +18,6 @@ module Gitdocs
18
18
 
19
19
  class Share < ActiveRecord::Base
20
20
  attr_accessible :polling_interval, :path, :notification, :branch_name, :remote_name
21
-
22
- def available_remotes
23
- repo = Grit::Repo.new(path)
24
- repo.remotes.map { |r| r.name }
25
- rescue
26
- nil
27
- end
28
-
29
- def available_branches
30
- repo = Grit::Repo.new(path)
31
- repo.heads.map { |r| r.name }
32
- rescue
33
- nil
34
- end
35
21
  end
36
22
 
37
23
  class Config < ActiveRecord::Base
@@ -11,18 +11,6 @@ module Gitdocs
11
11
  yield @config if block_given?
12
12
  end
13
13
 
14
- RepoDescriptor = Struct.new(:name, :index)
15
-
16
- def search(term)
17
- results = {}
18
- @runners.each_with_index do |runner, index|
19
- descriptor = RepoDescriptor.new(runner.root, index)
20
- repo_results = runner.search(term)
21
- results[descriptor] = repo_results unless repo_results.empty?
22
- end
23
- results
24
- end
25
-
26
14
  def start(web_port = nil)
27
15
  log("Starting Gitdocs v#{VERSION}...")
28
16
  log("Using configuration root: '#{config.config_root}'")
@@ -38,7 +26,8 @@ module Gitdocs
38
26
  # Start the web front-end
39
27
  if config.global.start_web_frontend
40
28
  web_port ||= config.global.web_frontend_port
41
- web_server = Server.new(self, web_port, *@runners)
29
+ repositories = config.shares.map { |x| Repository.new(x) }
30
+ web_server = Server.new(self, web_port, repositories)
42
31
  web_server.start
43
32
  web_server.wait_for_start_and_open(restarting)
44
33
  end
@@ -0,0 +1,38 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ # Wrapper for the UI notifier
4
+ class Gitdocs::Notifier
5
+ INFO_ICON = File.expand_path('../../img/icon.png', __FILE__)
6
+
7
+ def initialize(show_notifications)
8
+ @show_notifications = show_notifications
9
+ Guard::Notifier.turn_on if @show_notifications
10
+ end
11
+
12
+ def info(title, message)
13
+ if @show_notifications
14
+ Guard::Notifier.notify(message, title: title, image: INFO_ICON)
15
+ else
16
+ puts("#{title}: #{message}")
17
+ end
18
+ rescue # Prevent StandardErrors from stopping the daemon.
19
+ end
20
+
21
+ def warn(title, msg)
22
+ if @show_notifications
23
+ Guard::Notifier.notify(msg, title: title)
24
+ else
25
+ Kernel.warn("#{title}: #{msg}")
26
+ end
27
+ rescue # Prevent StandardErrors from stopping the daemon.
28
+ end
29
+
30
+ def error(title, message)
31
+ if @show_notifications
32
+ Guard::Notifier.notify(message, title: title, image: :failure)
33
+ else
34
+ Kernel.warn("#{title}: #{message}")
35
+ end
36
+ rescue # Prevent StandardErrors from stopping the daemon.
37
+ end
38
+ end
@@ -0,0 +1,348 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ # Wrapper for accessing the shared git repositories.
4
+ # Rugged, grit, or shell will be used in that order of preference depending
5
+ # upon the features which are available with each option.
6
+ #
7
+ # @note If a repository is invalid then query methods will return nil, and
8
+ # command methods will raise exceptions.
9
+ #
10
+ class Gitdocs::Repository
11
+ include ShellTools
12
+ attr_reader :invalid_reason
13
+
14
+ # Initialize the repository on the specified path. If the path is not valid
15
+ # for some reason, the object will be initialized but it will be put into an
16
+ # invalid state.
17
+ # @see #valid?
18
+ # @see #invalid_reason
19
+ #
20
+ # @param [String, Configuration::Share] path_or_share
21
+ def initialize(path_or_share)
22
+ path = path_or_share
23
+ if path_or_share.respond_to?(:path)
24
+ path = path_or_share.path
25
+ @remote_name = path_or_share.remote_name
26
+ @branch_name = path_or_share.branch_name
27
+ end
28
+
29
+ @rugged = Rugged::Repository.new(path)
30
+ @grit = Grit::Repo.new(path)
31
+ @invalid_reason = nil
32
+ rescue Rugged::OSError
33
+ @invalid_reason = :directory_missing
34
+ rescue Rugged::RepositoryError
35
+ @invalid_reason = :no_repository
36
+ end
37
+
38
+ # Clone a repository, and create the destination path if necessary.
39
+ #
40
+ # @param [String] path to clone the repository to
41
+ # @param [String] remote URI of the git repository to clone
42
+ #
43
+ # @raise [RuntimeError] if the clone fails
44
+ #
45
+ # @return [Gitdocs::Repository]
46
+ def self.clone(path, remote)
47
+ FileUtils.mkdir_p(File.dirname(path))
48
+ # TODO: determine how to do this with rugged, and handle SSH and HTTPS
49
+ # credentials.
50
+ Grit::Git.new(path).clone({ raise: true, quiet: true }, remote, path)
51
+
52
+ repository = new(path)
53
+ fail("Unable to clone into #{path}") unless repository.valid?
54
+ repository
55
+ rescue Grit::Git::GitTimeout => e
56
+ fail("Unable to clone into #{path} because it timed out")
57
+ rescue Grit::Git::CommandFailed => e
58
+ fail("Unable to clone into #{path} because of #{e.err}")
59
+ end
60
+
61
+ RepoDescriptor = Struct.new(:name, :index)
62
+
63
+ # Search across multiple repositories
64
+ #
65
+ # @param [String] term
66
+ # @param [Array<Repository>} repositories
67
+ #
68
+ # @return [Hash<RepoDescriptor, Array<SearchResult>>]
69
+ def self.search(term, repositories)
70
+ results = {}
71
+ repositories.each_with_index do |repository, index|
72
+ descriptor = RepoDescriptor.new(repository.root, index)
73
+ results[descriptor] = repository.search(term)
74
+ end
75
+ results.delete_if { |key, value| value.empty? }
76
+ end
77
+
78
+ SearchResult = Struct.new(:file, :context)
79
+
80
+ # Search a single repository
81
+ #
82
+ # @param [String] term
83
+ #
84
+ # @return [Array<SearchResult>]
85
+ def search(term)
86
+ return [] if term.empty?
87
+
88
+ results = []
89
+ options = { raise: true, bare: false, chdir: root, ignore_case: true }
90
+ @grit.git.grep(options, term).scan(/(.*?):([^\n]*)/) do |(file, context)|
91
+ if result = results.find { |s| s.file == file }
92
+ result.context += ' ... ' + context
93
+ else
94
+ results << SearchResult.new(file, context)
95
+ end
96
+ end
97
+ results
98
+ rescue Grit::Git::GitTimeout => e
99
+ # TODO: add logging to record the error details
100
+ []
101
+ rescue Grit::Git::CommandFailed => e
102
+ # TODO: add logging to record the error details if they are not just
103
+ # nothing found
104
+ []
105
+ end
106
+
107
+ # @return [String]
108
+ def root
109
+ return nil unless valid?
110
+ @rugged.path.sub(/.\.git./, '')
111
+ end
112
+
113
+ # @return [Boolean]
114
+ def valid?
115
+ !@invalid_reason
116
+ end
117
+
118
+ # @return [nil] if the repository is invalid
119
+ # @return [Array<String>] sorted list of remote branches
120
+ def available_remotes
121
+ return nil unless valid?
122
+ Rugged::Branch.each_name(@rugged, :remote).sort
123
+ end
124
+
125
+ # @return [nil] if the repository is invalid
126
+ # @return [Array<String>] sorted list of local branches
127
+ def available_branches
128
+ return nil unless valid?
129
+ Rugged::Branch.each_name(@rugged, :local).sort
130
+ end
131
+
132
+ # @return [String] oid of the HEAD of the working directory
133
+ def current_oid
134
+ @rugged.head.target
135
+ rescue Rugged::ReferenceError
136
+ nil
137
+ end
138
+
139
+ # Fetch and merge the repository
140
+ #
141
+ # @raise [RuntimeError] if there is a problem processing conflicted files
142
+ #
143
+ # @return [nil] if the repository is invalid
144
+ # @return [:no_remote] if the remote is not yet set
145
+ # @return [String] if there is an error return the message
146
+ # @return [Array<String>] if there is a conflict return the Array of
147
+ # conflicted file names
148
+ # @return [:ok] if pulled and merged with no errors or conflicts
149
+ def pull
150
+ return nil unless valid?
151
+ return :no_remote unless has_remote?
152
+
153
+ out, status = sh_with_code("cd #{root} ; git fetch --all 2>/dev/null && git merge #{@remote_name}/#{@branch_name} 2>/dev/null")
154
+
155
+ if status.success?
156
+ :ok
157
+ elsif out[/CONFLICT/]
158
+ # Find the conflicted files
159
+ conflicted_files = sh('git ls-files -u --full-name -z').split("\0")
160
+ .reduce(Hash.new { |h, k| h[k] = [] }) do|h, line|
161
+ parts = line.split(/\t/)
162
+ h[parts.last] << parts.first.split(/ /)
163
+ h
164
+ end
165
+
166
+ # Mark the conflicted files
167
+ conflicted_files.each do |conflict, ids|
168
+ conflict_start, conflict_end = conflict.scan(/(.*?)(|\.[^\.]+)$/).first
169
+ ids.each do |(mode, sha, id)|
170
+ author = ' original' if id == '1'
171
+ system("cd #{root} && git show :#{id}:#{conflict} > '#{conflict_start} (#{sha[0..6]}#{author})#{conflict_end}'")
172
+ end
173
+ system("cd #{root} && git rm --quiet #{conflict} >/dev/null 2>/dev/null") || fail
174
+ end
175
+
176
+ conflicted_files.keys
177
+ else
178
+ out # return the output on error
179
+ end
180
+ end
181
+
182
+ # Commit and push the repository
183
+ #
184
+ # @return [nil] if the repository is invalid
185
+ # @return [:no_remote] if the remote is not yet set
186
+ # @return [:nothing] if there was nothing to do
187
+ # @return [String] if there is an error return the message
188
+ # @return [:ok] if commited and pushed without errors or conflicts
189
+ def push(last_synced_oid, message='Auto-commit from gitdocs')
190
+ return nil unless valid?
191
+ return :no_remote unless has_remote?
192
+
193
+ #add and commit
194
+ sh_string('find . -type d -regex ``./[^.].*'' -empty -exec touch \'{}/.gitignore\' \;')
195
+ sh_string('git add .')
196
+ sh_string("git commit -a -m #{ShellTools.escape(message)}") unless sh("cd #{root} ; git status -s").empty?
197
+
198
+ if last_synced_oid.nil? || sh_string('git status')[/branch is ahead/]
199
+ out, code = sh_with_code("git push #{@remote_name} #{@branch_name}")
200
+ if code.success?
201
+ :ok
202
+ elsif last_synced_oid.nil?
203
+ :nothing
204
+ elsif out[/\[rejected\]/]
205
+ :conflict
206
+ else
207
+ out # return the output on error
208
+ end
209
+ else
210
+ :nothing
211
+ end
212
+ end
213
+
214
+ # Get the count of commits by author from the head to the specified oid.
215
+ #
216
+ # @param [String] last_oid
217
+ #
218
+ # @return [Hash<String, Int>]
219
+ def author_count(last_oid)
220
+ walker = head_walker
221
+ walker.hide(last_oid) if last_oid
222
+ walker.inject(Hash.new(0)) do |result, commit|
223
+ result["#{commit.author[:name]} <#{commit.author[:email]}>"] += 1
224
+ result
225
+ end
226
+ rescue Rugged::ReferenceError
227
+ {}
228
+ rescue Rugged::OdbError
229
+ {}
230
+ end
231
+
232
+ # Returns file meta data based on relative file path
233
+ #
234
+ # @example
235
+ # file_meta("path/to/file")
236
+ # => { :author => "Nick", :size => 1000, :modified => ... }
237
+ #
238
+ # @param [String] file relative path to file in repository
239
+ #
240
+ # @raise [RuntimeError] if the file is not found in any commits
241
+ #
242
+ # @return [Hash<Symbol=>String,Integer,Time>] the author, size and
243
+ # modification date of the file
244
+ def file_meta(file)
245
+ file = file.gsub(%r{^/}, '')
246
+
247
+ commit = head_walker.find { |x| x.diff(paths: [file]).size > 0 }
248
+
249
+ fail "File #{file} not found" unless commit
250
+
251
+ full_path = File.expand_path(file, root)
252
+ size = if File.directory?(full_path)
253
+ Dir[File.join(full_path, '**', '*')].reduce(0) do |size, file|
254
+ File.symlink?(file) ? size : size += File.size(file)
255
+ end
256
+ else
257
+ File.symlink?(full_path) ? 0 : File.size(full_path)
258
+ end
259
+ size = -1 if size == 0 # A value of 0 breaks the table sort for some reason
260
+
261
+ { author: commit.author[:name], size: size, modified: commit.author[:time] }
262
+ end
263
+
264
+ # Returns the revisions available for a particular file
265
+ #
266
+ # @example
267
+ # file_revisions("README")
268
+ #
269
+ # @param [String] file
270
+ #
271
+ # @return [Array<Hash>]
272
+ def file_revisions(file)
273
+ file = file.gsub(%r{^/}, '')
274
+ # Excluding the initial commit (without a parent) which keeps things
275
+ # consistent with the original behaviour.
276
+ # TODO: reconsider if this is the correct behaviour
277
+ head_walker.select{|x| x.parents.size == 1 && x.diff(paths: [file]).size > 0 }
278
+ .first(100)
279
+ .map do |commit|
280
+ {
281
+ commit: commit.oid[0, 7],
282
+ subject: commit.message.split("\n")[0],
283
+ author: commit.author[:name],
284
+ date: commit.author[:time]
285
+ }
286
+ end
287
+ end
288
+
289
+ # Put the contents of the specified file revision into a temporary file
290
+ #
291
+ # @example
292
+ # file_revision_at("README", "a4c56h")
293
+ # => "/tmp/some/path/README"
294
+ #
295
+ # @param [String] file
296
+ # @param [String] ref
297
+ #
298
+ # @return [String] path of the temporary file
299
+ def file_revision_at(file, ref)
300
+ file = file.gsub(%r{^/}, '')
301
+ content = @rugged.blob_at(ref, file).text
302
+ tmp_path = File.expand_path(File.basename(file), Dir.tmpdir)
303
+ File.open(tmp_path, 'w') { |f| f.puts content }
304
+ tmp_path
305
+ end
306
+
307
+ # Revert file to the specified ref
308
+ #
309
+ # @param [String] file
310
+ # @param [String] ref
311
+ def file_revert(file, ref)
312
+ file = file.gsub(%r{^/}, '')
313
+ blob = @rugged.blob_at(ref, file)
314
+ # Silently fail if the file/ref do not existing in the repository.
315
+ # Which is consistent with the original behaviour.
316
+ # TODO: should consider throwing an exception on this condition
317
+ return unless blob
318
+
319
+ File.open(File.expand_path(file, root), 'w') { |f| f.puts(blob.text) }
320
+ end
321
+
322
+ ##############################################################################
323
+
324
+ private
325
+
326
+ def has_remote?
327
+ sh_string('git remote')
328
+ end
329
+
330
+ def head_walker
331
+ walker = Rugged::Walker.new(@rugged)
332
+ walker.sorting(Rugged::SORT_DATE)
333
+ walker.push(@rugged.head.target)
334
+ walker
335
+ end
336
+
337
+ # sh_string("git config branch.`git branch | grep '^\*' | sed -e 's/\* //'`.remote", "origin")
338
+ def sh_string(cmd, default = nil)
339
+ val = sh("cd #{root} ; #{cmd}").strip rescue nil
340
+ val.nil? || val.empty? ? default : val
341
+ end
342
+
343
+ # Run in shell, return both status and output
344
+ # @see #sh
345
+ def sh_with_code(cmd)
346
+ ShellTools.sh_with_code(cmd, root)
347
+ end
348
+ end