grack 0.0.2 → 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +7 -0
  3. data/.yardopts +1 -0
  4. data/LICENSE +22 -0
  5. data/NEWS.md +19 -0
  6. data/README.md +211 -0
  7. data/Rakefile +222 -0
  8. data/lib/git_adapter.rb +1 -0
  9. data/lib/grack.rb +1 -0
  10. data/lib/grack/app.rb +482 -0
  11. data/lib/grack/compatible_git_adapter.rb +99 -0
  12. data/lib/grack/file_streamer.rb +41 -0
  13. data/lib/grack/git_adapter.rb +142 -0
  14. data/lib/grack/io_streamer.rb +45 -0
  15. data/tests/app_test.rb +534 -0
  16. data/tests/compatible_git_adapter_test.rb +137 -0
  17. data/tests/example/_git/COMMIT_EDITMSG +1 -0
  18. data/tests/example/_git/HEAD +1 -0
  19. data/tests/example/_git/config +6 -0
  20. data/tests/example/_git/description +1 -0
  21. data/tests/example/_git/hooks/applypatch-msg.sample +15 -0
  22. data/tests/example/_git/hooks/commit-msg.sample +24 -0
  23. data/tests/example/_git/hooks/post-commit.sample +8 -0
  24. data/tests/example/_git/hooks/post-receive.sample +15 -0
  25. data/tests/example/_git/hooks/post-update.sample +8 -0
  26. data/tests/example/_git/hooks/pre-applypatch.sample +14 -0
  27. data/tests/example/_git/hooks/pre-commit.sample +50 -0
  28. data/tests/example/_git/hooks/pre-rebase.sample +169 -0
  29. data/tests/example/_git/hooks/prepare-commit-msg.sample +36 -0
  30. data/tests/example/_git/hooks/update.sample +128 -0
  31. data/tests/example/_git/index +0 -0
  32. data/tests/example/_git/info/exclude +6 -0
  33. data/tests/example/_git/info/refs +1 -0
  34. data/tests/example/_git/logs/HEAD +1 -0
  35. data/tests/example/_git/logs/refs/heads/master +1 -0
  36. data/tests/example/_git/objects/31/d73eb4914a8ddb6cb0e4adf250777161118f90 +0 -0
  37. data/tests/example/_git/objects/cb/067e06bdf6e34d4abebf6cf2de85d65a52c65e +0 -0
  38. data/tests/example/_git/objects/ce/013625030ba8dba906f756967f9e9ca394464a +0 -0
  39. data/tests/example/_git/objects/info/packs +2 -0
  40. data/tests/example/_git/objects/pack/pack-62c9f443d8405cd6da92dcbb4f849cc01a339c06.idx +0 -0
  41. data/tests/example/_git/objects/pack/pack-62c9f443d8405cd6da92dcbb4f849cc01a339c06.pack +0 -0
  42. data/tests/example/_git/refs/heads/master +1 -0
  43. data/tests/file_streamer_test.rb +37 -0
  44. data/tests/git_adapter_test.rb +104 -0
  45. data/tests/io_streamer_test.rb +36 -0
  46. data/tests/test_helper.rb +36 -0
  47. metadata +292 -19
  48. data/lib/git_http.rb +0 -304
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cb383037da24ee5963414d2d7dee6cb496b602fe
4
+ data.tar.gz: df32f8f77576e5602c9d6e77970083b30fd69a9c
5
+ SHA512:
6
+ metadata.gz: df02a597393545859f15d6c94136c4c61e9c291943ecc96d7271a62b21fa3d0bd45383a13b874db89c4bb0009fc2e333aac25c159f72052fc5f20a4bdbae779a
7
+ data.tar.gz: 711850b57b041acd41e2fa8809fb643549ac640ad1d358ccea47a97ebd41763b869ed39c8054f2da1e3f9a791b1d2bfbd2d524e4df4981967c8bcee144b7fb61
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.0
5
+ - 2.1.1
6
+ - 2.2.2
7
+ script: bundle exec rake test
@@ -0,0 +1 @@
1
+ --protected --private --main README.md lib/**/*.rb - NEWS.md LICENSE
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2015 Scott Chacon <schacon@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/NEWS.md ADDED
@@ -0,0 +1,19 @@
1
+ # News and Notifications by Version
2
+
3
+ This file lists noteworthy changes which may affect users of this project. More
4
+ detailed information is available in the rest of the documentation.
5
+
6
+ **NOTE:** Date stamps in the following entries are in YYYY/MM/DD format.
7
+
8
+ ## v0.1.0.pre1
9
+
10
+ * Moved projects to [grackorg/grack](https://github.com/grackorg/grack)
11
+ * Test release for major rewrite. See https://github.com/grackorg/grack/pull/3.
12
+
13
+ ## v0.0.2 (2012/10/10)
14
+
15
+ * Fix the file list in the gem.
16
+
17
+ ## v0.0.1 (2012/10/10)
18
+
19
+ * Birthday
@@ -0,0 +1,211 @@
1
+ [![Gem Version](https://badge.fury.io/rb/grack.svg)](http://badge.fury.io/rb/grack)
2
+ [![Build Status](https://travis-ci.org/grackorg/grack.svg?branch=master)](https://travis-ci.org/grackorg/grack)
3
+ [![Dependency Status](https://gemnasium.com/grackorg/grack.svg)](https://gemnasium.com/grackorg/grack)
4
+
5
+ # Grack - Ruby/Rack Git Smart HTTP Server Handler
6
+
7
+ This project aims to replace the builtin git-http-backend CGI handler
8
+ distributed with C Git with a Rack application.
9
+
10
+ ## Links
11
+
12
+ * Homepage :: https://github.com/grackorg/grack
13
+ * Source :: https://github.com/grackorg/grack.git
14
+
15
+ ## Description
16
+
17
+ This project aims to replace the builtin git-http-backend CGI handler
18
+ distributed with C Git with a Rack application. By default, Grack uses calls to
19
+ git on the system to implement Smart HTTP. Since the git-http-backend is really
20
+ just a simple wrapper for the upload-pack and receive-pack processes with the
21
+ '--stateless-rpc' option, this does not actually re-implement very much.
22
+ However, it is possible to use a different backend by specifying a different
23
+ Adapter.
24
+
25
+ The default git-http-backend only runs as a CGI script, and specifically is
26
+ only targeted for Apache 2.x usage (it requires PATH_INFO to be set and
27
+ specifically formatted). So, instead of trying to get it to work with other
28
+ CGI capable webservers (Lighttpd, etc), we can get it running on nearly every
29
+ major and minor webserver out there by making it Rack capable. Rack
30
+ applications can run with the following handlers:
31
+
32
+ * CGI
33
+ * FCGI
34
+ * Mongrel (and EventedMongrel and SwiftipliedMongrel)
35
+ * WEBrick
36
+ * SCGI
37
+ * LiteSpeed
38
+ * Thin
39
+
40
+ These web servers include Rack handlers in their distributions:
41
+
42
+ * Ebb
43
+ * Fuzed
44
+ * Phusion Passenger (which is mod_rack for Apache and for nginx)
45
+ * Unicorn
46
+
47
+ With [Warbler](http://caldersphere.rubyforge.org/warbler/classes/Warbler.html),
48
+ and JRuby, we can also generate a WAR file that can be deployed in any Java web
49
+ application server (Tomcat, Glassfish, Websphere, JBoss, etc).
50
+
51
+ By default, Grack uses calls to git on the system to implement Smart HTTP.
52
+ Since the git-http-backend is really just a simple wrapper for the upload-pack
53
+ and receive-pack processes with the '--stateless-rpc' option, this does not
54
+ actually re-implement very much. However, it is possible to use a different
55
+ backend by specifying a different Adapter. See below for a list.
56
+
57
+ Note that while it is technically possible to host non-bare repositories with
58
+ this gem, it is discouraged. The only somewhat safe option is to serve such a
59
+ repository as read-only since there is a greater risk of arbitrary filesystem
60
+ traversal when a checkout tree must be traversed to reach the repository
61
+ administrative area (`.git` directory). Additionally, any recent version of Git
62
+ prevents pushes into non-bare repositories by default since pushing into the
63
+ currently checked out branch can effectively "break" the checkout tree.
64
+
65
+ ### Git Adapters
66
+
67
+ Grack makes calls to the git binary through the GitAdapter abstraction class.
68
+ Grack can be made to use a different backend by specifying a call-able object,
69
+ such as a lambda, in Grack's configuration that provides new adapter instances
70
+ per request. For example:
71
+
72
+ ```ruby
73
+ Grack::App.new(:git_adapter_factory => ->{ MyAdapter.new })
74
+ ```
75
+
76
+ Alternative adapters available:
77
+ * [rjgit_grack](http://github.com/dometto/rjgit_grack) lets Grack use the
78
+ [RJGit](http://github.com/repotag/rjgit) gem to implement Smart HTTP in pure
79
+ Jruby. (Currently requires use of backward compatibility support via
80
+ Grack::CompatibleGitAdapter)
81
+
82
+ ### Developing Adapters
83
+
84
+ Adapters are abstraction classes that handle the actual implementation of the
85
+ Smart HTTP protocol (advertising refs, uploading and receiving packfiles). Such
86
+ abstraction classes must have the following methods:
87
+
88
+ ```ruby
89
+ MyAdapter.repository_path=(repository_path)
90
+ MyAdapter.exist?
91
+ MyAdapter.handle_pack(kind, io_in, io_out, opts = {})
92
+ MyAdapter.file(path)
93
+ MyAdapter.update_server_info
94
+ MyAdapter.allow_push?
95
+ MyAdapter.allow_pull?
96
+ ```
97
+
98
+ See `Grack::GitAdapter` for more detailed documentation and an example
99
+ implementation.
100
+
101
+ ## Features
102
+
103
+ * Supports Git Smart HTTP protocol.
104
+ * Supports Git Basic HTTP protocol.
105
+ * Limits push/pull access globally and per-repository.
106
+ * Thread safe operation.
107
+
108
+ ## Known Bugs/Limitations
109
+
110
+ * Will likely block fully evented web servers when using the stock Git adapter.
111
+
112
+ ## Synopsis
113
+
114
+ In `config.ru`:
115
+
116
+ ```ruby
117
+ require 'grack/app'
118
+ require 'grack/git_adapter'
119
+
120
+ config = {
121
+ :root => '/path/to/bare/repositories',
122
+ :allow_push => true,
123
+ :allow_pull => true,
124
+ :git_adapter_factory => ->{ GitAdapter.new }
125
+ }
126
+
127
+ run Grack::App.new(config)
128
+ ```
129
+
130
+ Then run:
131
+
132
+ ```sh
133
+ $ bundle exec rackup --host localhost --port 8080 config.ru
134
+ $ git clone http://localhost:8080/your-repository.git
135
+ ```
136
+
137
+ ## Runtime Requirements
138
+
139
+ * Ruby >=1.9.3
140
+ * Git >=1.7 (if using the included Git adapter)
141
+ * rack (>= 0)
142
+
143
+ ## Development Requirements
144
+
145
+ * All runtime requirements
146
+ * rake (>= 10.1.1, ~> 10.1)
147
+ * rack-test (>= 0.6.3, ~> 0.6)
148
+ * minitest (>= 5.8.0, ~> 5.8)
149
+ * mocha (>= 1.1.0, ~> 1.1)
150
+ * simplecov (>= 0.10.0, ~> 0.10)
151
+ * yard (>= 0.8.7.3, ~> 0.8.7)
152
+ * redcarpet (>= 3.1.0, ~> 3.1)
153
+ * github-markup (>= 1.0.2, ~> 1.0)
154
+ * pry (~> 0)
155
+
156
+ ## Contributing
157
+
158
+ Contributions for bug fixes, documentation, extensions, tests, etc. are
159
+ encouraged.
160
+
161
+ 1. Clone the repository.
162
+ 2. Fix a bug or add a feature.
163
+ 3. Add tests for the fix or feature.
164
+ 4. Make a pull request.
165
+
166
+ ## Development
167
+
168
+ After checking out the source, run:
169
+
170
+ ```sh
171
+ $ bundle install
172
+ $ bundle exec rake test yard
173
+ ```
174
+
175
+ This will install all dependencies, run the tests/specs, and generate the
176
+ documentation.
177
+
178
+ ## Authors
179
+
180
+ Thanks to all contributors. Without your help this project would not exist.
181
+
182
+ * Scott Chacon :: schacon@gmail.com
183
+ * Dawa Ometto :: dawa.ometto@phil.uu.nl
184
+ * Jeremy Bopp :: jeremy@bopp.net
185
+
186
+ ## License
187
+
188
+ ```
189
+ (The MIT License)
190
+
191
+ Copyright (c) 2015 Scott Chacon <schacon@gmail.com>
192
+
193
+ Permission is hereby granted, free of charge, to any person obtaining
194
+ a copy of this software and associated documentation files (the
195
+ 'Software'), to deal in the Software without restriction, including
196
+ without limitation the rights to use, copy, modify, merge, publish,
197
+ distribute, sublicense, and/or sell copies of the Software, and to
198
+ permit persons to whom the Software is furnished to do so, subject to
199
+ the following conditions:
200
+
201
+ The above copyright notice and this permission notice shall be
202
+ included in all copies or substantial portions of the Software.
203
+
204
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
205
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
206
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
207
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
208
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
209
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
210
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
211
+ ```
@@ -0,0 +1,222 @@
1
+ # encoding: UTF-8
2
+ # -*- ruby -*-
3
+
4
+ require 'erb'
5
+ require 'rake/testtask'
6
+ require 'rubygems/package_task'
7
+ require 'rake/clean'
8
+ require 'yard'
9
+
10
+ # Load the gemspec file for this project.
11
+ GEMSPEC = Dir['*.gemspec'].first
12
+ SPEC = eval(File.read(GEMSPEC), nil, GEMSPEC)
13
+
14
+ # A dynamically generated list of files that should match the manifest (the
15
+ # combined contents of SPEC.files and SPEC.test_files). The idea is for this
16
+ # list to contain all project files except for those that have been explicitly
17
+ # excluded. This list will be compared with the manifest from the SPEC in order
18
+ # to help catch the addition or removal of files to or from the project that
19
+ # have not been accounted for either by an exclusion here or an inclusion in the
20
+ # SPEC manifest.
21
+ #
22
+ # NOTE:
23
+ # It is critical that the manifest is *not* automatically generated via globbing
24
+ # and the like; otherwise, this will yield a simple comparison between
25
+ # redundantly generated lists of files that probably will not protect the
26
+ # project from the unintentional inclusion or exclusion of files in the
27
+ # distribution.
28
+ PKG_FILES = FileList.new(Dir.glob('**/*', File::FNM_DOTMATCH)) do |files|
29
+ # Exclude anything that doesn't exist as well as directories.
30
+ files.exclude {|file| ! File.exist?(file) || File.directory?(file)}
31
+ # Exclude Git administrative files.
32
+ files.exclude('.git/**/*', '**/.gitignore', '**/.gitmodule', '**/.gitkeep')
33
+ # Exclude editor swap/temporary files.
34
+ files.exclude('**/.*.sw?', '**/.sw?')
35
+ # Exclude the gemspec file.
36
+ files.exclude(GEMSPEC)
37
+ # Exclude the README template file.
38
+ files.exclude('README.md.erb')
39
+ # Exclude resources for bundler.
40
+ files.exclude('Gemfile', 'Gemfile.lock')
41
+ files.exclude(%r{^.bundle([/\\]|$)})
42
+ files.exclude(%r{^vendor/bundle([/\\]|$)})
43
+ # Exclude generated content, except for the README file.
44
+ files.exclude(%r{^(pkg|doc|coverage|.yardoc)([/\\]|$)})
45
+ # Exclude examples.
46
+ files.exclude('examples/**/*')
47
+ # Exclude Rubinius compiled Ruby files.
48
+ files.exclude('**/*.rbc')
49
+ end
50
+
51
+ # Make sure that :clean and :clobber will not whack the repository files.
52
+ CLEAN.exclude('.git/**')
53
+ # Vim swap files are fair game for clean up.
54
+ CLEAN.include('**/.*.sw?')
55
+
56
+ # Returns the value of the VERSION environment variable as a Gem::Version object
57
+ # assuming it is set and a valid Gem version string. Otherwise, raises an
58
+ # exception.
59
+ def get_version_argument
60
+ version = ENV['VERSION']
61
+ if version.to_s.empty?
62
+ raise "No version specified: Add VERSION=X.Y.Z to the command line"
63
+ end
64
+ begin
65
+ Gem::Version.create(version.dup)
66
+ rescue ArgumentError
67
+ raise "Invalid version specified in `VERSION=#{version}'"
68
+ end
69
+ end
70
+
71
+ # Performs an in place, per line edit of the file indicated by _path_ by calling
72
+ # the sub method on each line and passing _pattern_, _replacement_, and _b_ as
73
+ # arguments.
74
+ def file_sub(path, pattern, replacement = nil, &b)
75
+ tmp_path = "#{path}.tmp"
76
+ File.open(path) do |infile|
77
+ File.open(tmp_path, 'w') do |outfile|
78
+ infile.each do |line|
79
+ outfile.write(line.sub(pattern, replacement, &b))
80
+ end
81
+ end
82
+ end
83
+ File.rename(tmp_path, path)
84
+ end
85
+
86
+ # Updates the version string in the gemspec file to the string in _version_.
87
+ def set_version(version)
88
+ file_sub(GEMSPEC, /(\.version\s*=\s*).*/, "\\1'#{version}'")
89
+ end
90
+
91
+ # Returns a string that is line wrapped at word boundaries, where each line is
92
+ # no longer than _line_width_ characters.
93
+ #
94
+ # This is mostly lifted directly from ActionView::Helpers::TextHelper.
95
+ def word_wrap(text, line_width = 80)
96
+ text.split("\n").collect do |line|
97
+ line.length > line_width ?
98
+ line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip :
99
+ line
100
+ end * "\n"
101
+ end
102
+
103
+ desc 'Alias for build:gem'
104
+ task :build => 'build:gem'
105
+
106
+ # Build related tasks.
107
+ namespace :build do
108
+ # Create the gem and package tasks.
109
+ Gem::PackageTask.new(SPEC).define
110
+
111
+ # Ensure that the manifest is consulted when building the gem. Any
112
+ # generated/compiled files should be available at that time.
113
+ task :gem => :check_manifest
114
+
115
+ desc 'Verify the manifest'
116
+ task :check_manifest do
117
+ manifest_files = (SPEC.files + SPEC.test_files).sort.uniq
118
+ pkg_files = PKG_FILES.sort.uniq
119
+ if manifest_files != pkg_files then
120
+ common_files = manifest_files & pkg_files
121
+ manifest_files -= common_files
122
+ pkg_files -= common_files
123
+ message = ["The manifest does not match the automatic file list."]
124
+ unless manifest_files.empty? then
125
+ message << " Extraneous files:\n " + manifest_files.join("\n ")
126
+ end
127
+ unless pkg_files.empty?
128
+ message << " Missing files:\n " + pkg_files.join("\n ")
129
+ end
130
+ raise message.join("\n")
131
+ end
132
+ end
133
+
134
+ # Creates the README.md file from its template and other sources.
135
+ file 'README.md' => ['README.md.erb', 'LICENSE', GEMSPEC] do
136
+ spec = SPEC
137
+ File.open('README.md', 'w') do |readme|
138
+ readme.write(
139
+ ERB.new(File.read('README.md.erb'), nil, '-').result(binding)
140
+ )
141
+ end
142
+ end
143
+ end
144
+
145
+ # Ensure that the clobber task also clobbers package files.
146
+ task :clobber => 'build:clobber_package'
147
+
148
+ # Create the documentation task.
149
+ YARD::Rake::YardocTask.new
150
+ # Ensure that the README file is (re)generated first.
151
+ task :yard => 'README.md'
152
+
153
+ # Gem related tasks.
154
+ namespace :gem do
155
+ desc 'Alias for build:gem'
156
+ task :build => 'build:gem'
157
+
158
+ desc 'Publish the gemfile'
159
+ task :publish => ['version:check', :test, 'repo:tag', :build] do
160
+ sh "gem push pkg/#{SPEC.name}-#{SPEC.version}*.gem"
161
+ end
162
+ end
163
+
164
+ Rake::TestTask.new do |t|
165
+ t.pattern = 'tests/**/*_test.rb'
166
+ end
167
+
168
+ # Version string management tasks.
169
+ namespace :version do
170
+ desc 'Set the version for the project to a specified version'
171
+ task :set do
172
+ set_version(get_version_argument)
173
+ end
174
+
175
+ desc 'Set the version for the project back to 0.0.0'
176
+ task :reset do
177
+ set_version('0.0.0')
178
+ end
179
+
180
+ desc 'Check that all version strings are correctly set'
181
+ task :check => ['version:check:spec', 'version:check:news']
182
+
183
+ namespace :check do
184
+ desc 'Check that the version in the gemspec is correctly set'
185
+ task :spec do
186
+ version = get_version_argument
187
+ if version != SPEC.version
188
+ raise "The given version `#{version}' does not match the gemspec version `#{SPEC.version}'"
189
+ end
190
+ end
191
+
192
+ desc 'Check that the NEWS.md file mentions the version'
193
+ task :news do
194
+ version = get_version_argument
195
+ begin
196
+ File.open('NEWS.md') do |news|
197
+ unless news.each_line.any? {|l| l =~ /^## v#{Regexp.escape(version.to_s)} /}
198
+ raise "The NEWS.md file does not mention version `#{version}'"
199
+ end
200
+ end
201
+ rescue Errno::ENOENT
202
+ raise 'No NEWS.md file found'
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ # Repository and workspace management tasks.
209
+ namespace :repo do
210
+ desc 'Tag the current HEAD with the version string'
211
+ task :tag => :check_workspace do
212
+ version = get_version_argument
213
+ sh "git tag -s -m 'Release v#{version}' v#{version}"
214
+ end
215
+
216
+ desc 'Ensure the workspace is fully committed and clean'
217
+ task :check_workspace => ['README.md'] do
218
+ unless `git status --untracked-files=all --porcelain`.empty?
219
+ raise 'Workspace has been modified. Commit pending changes and try again.'
220
+ end
221
+ end
222
+ end