gitdocs 0.5.0.pre1 → 0.5.0.pre2

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.
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