tendersync 1.0.2

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.
data/History.txt ADDED
@@ -0,0 +1,13 @@
1
+ == 1.0.2 2009-08-15
2
+
3
+ * Fixed some bugs in posting
4
+
5
+ == 1.0.1 2009-07-17
6
+
7
+ * Indexing working
8
+ * More command line options
9
+ * Tests and Documentation
10
+
11
+ == 1.0.0 2009-06-11
12
+
13
+ * First Release
data/Manifest ADDED
@@ -0,0 +1,27 @@
1
+ bin/tendersync
2
+ config/website.yml.sample
3
+ History.txt
4
+ lib/tendersync/document.rb
5
+ lib/tendersync/runner.rb
6
+ lib/tendersync/session.rb
7
+ lib/tendersync/tendersync.rb
8
+ lib/tendersync.rb
9
+ Manifest
10
+ Rakefile
11
+ README.md
12
+ script/console
13
+ script/destroy
14
+ script/generate
15
+ script/txt2html
16
+ spec/fixtures/passenger_restart_issues
17
+ spec/spec.opts
18
+ spec/spec_helper.rb
19
+ spec/tendersync_document_spec.rb
20
+ spec/tendersync_session_spec.rb
21
+ spec/tendersync_spec.rb
22
+ tasks/rspec.rake
23
+ website/index.html
24
+ website/index.txt
25
+ website/javascripts/rounded_corners_lite.inc.js
26
+ website/stylesheets/screen.css
27
+ website/template.html.erb
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Tendersync
2
+
3
+ Authors: Markus Roberts and Bill Kayser
4
+
5
+ Tendersync allows you to sync documents stored in the
6
+ [ENTP Tender](http://www.tenderapp.com)
7
+ `faqs` section with a local filesystem, allowing you to manage your
8
+ documents with git or subversion.
9
+
10
+ It includes a command for creating an index document for any given
11
+ section.
12
+
13
+ Find out more about Tender by visiting [the Tender site](http://www.tenderapp.com).
14
+
15
+ ## Features
16
+
17
+ * List remote sections and documents under the `/faqs` area
18
+ * Pull single documents or entire tree from Tender site to local
19
+ filesystem
20
+ * Push local changes back to Tender one at a time or en masse
21
+ * Manage document meta-data, like keywords, in headers
22
+ * Push changed versions to the server
23
+
24
+ ## Synopsis
25
+
26
+ Create a working directory where you want to store the tender docs in a hierarchy
27
+ and run tendersync from there.
28
+
29
+ sudo gem install tendersync
30
+ cd $workdir
31
+ tendersync -h
32
+
33
+ ## Using Tendersync
34
+
35
+ To get started, you need to pass in your account information. You
36
+ only need to do this once. A local file `.tendersync` is created with
37
+ the configuration information.
38
+
39
+ This will get you set up:
40
+
41
+ tendersync -u user@me.com -p password --docurl=http://company.tenderapp.com
42
+
43
+ To verify it worked run the `ls` command:
44
+
45
+ tendersync ls
46
+
47
+ Tender documents are organized into sections defined by you. At New
48
+ Relic, we have faqs, docs, and troubleshooting. You can specify
49
+ commands to apply to one or more sections by passing in section names
50
+ with -s:
51
+
52
+ tendersync -s docs -s troubleshooting pull
53
+
54
+ ### Examples
55
+
56
+ Start with:
57
+
58
+ tendersync -h
59
+
60
+ Download all your docs:
61
+
62
+ tendersync pull
63
+
64
+ Download just the faq docs:
65
+
66
+ tendersync pull -s faqs
67
+
68
+ Create a git repository and save all the documents:
69
+
70
+ git init
71
+ git add .
72
+ git commit -m "First version of docs on Tender"
73
+
74
+ Upload docs to the server:
75
+
76
+ tendersync post faqs/sinatra_support
77
+ tendersync post docs/install-*
78
+ tendersync post -s docs
79
+
80
+ Upload everything to the server (regardless of whether the content has
81
+ changed or not):
82
+
83
+ tendersync post
84
+
85
+ ### Using the `index` Command
86
+
87
+ You can generate a table of contents for any section with the index
88
+ command. By default index will generate a single file named
89
+ `SECTION_table_of_contents`.
90
+
91
+ In this file will be a list of all the files in the given section with
92
+ links to those files. Under each file link will be a bullet list of
93
+ the topmost sections in the document. If these sections are preceeded
94
+ by anchor links (A elements with the name attribute) then the bullets
95
+ will have links to those sections.
96
+
97
+ It will look something like this:
98
+
99
+ <pre>
100
+ ## Installation and configuration
101
+ ### [Agent Installation (Ruby)](agent-installation)
102
+ * [Installing the Plug-in](agent-installation#Installing_the_Plug-in)
103
+ * [Installing the Gem](agent-installation#Installing_the_Gem)
104
+ </pre>
105
+
106
+ #### Customizing the amount of detail in the Index
107
+
108
+ You can show sections deeper than one level in a particular document
109
+ using the `-d` option. The default is 1.
110
+
111
+ tendersync index -d 2
112
+
113
+ #### Definiting TOC groups
114
+
115
+ If you want to divide the table of contents into groups of related
116
+ documents, you can pass in a title for a group and a regular expression
117
+ to match against document titles that belong in that group. These group
118
+ definitions will be saved so you only need to enter them once.
119
+
120
+ Enter a group using the `-g` option passing in a title and regular
121
+ expression separated by a semi-colon.
122
+
123
+ tendersync index -g "Page Details;/page/i"
124
+
125
+ You can add multiple groups with additional -g options:
126
+
127
+ tendersync index -g "Page Details;/page/i" -g "Installation Info;/installation/i"
128
+
129
+ If you want to remove a group definition, you need to remove it
130
+ manually from the `.tendersync` file.
131
+
132
+ ## THANKS
133
+
134
+ All due regards, credit, thanks, etc., to the ENTP team for a great tool.
135
+
136
+ ## LICENSE
137
+
138
+ Copyright (c) 2009 New Relic, Inc.
139
+
140
+ Permission is hereby granted, free of charge, to any person obtaining
141
+ a copy of this software and associated documentation files (the
142
+ 'Software'), to deal in the Software without restriction, including
143
+ without limitation the rights to use, copy, modify, merge, publish,
144
+ distribute, sublicense, and/or sell copies of the Software, and to
145
+ permit persons to whom the Software is furnished to do so, subject to
146
+ the following conditions:
147
+
148
+ The above copyright notice and this permission notice shall be
149
+ included in all copies or substantial portions of the Software.
150
+
151
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
152
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
153
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
154
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
155
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
156
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
157
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'echoe'
3
+ %w[rake rake/clean fileutils newgem rubigen].each { |f| require f }
4
+ require File.dirname(__FILE__) + '/lib/tendersync'
5
+
6
+ GEM_NAME = "tendersync"
7
+ GEM_VERSION = Tendersync::VERSION
8
+ AUTHOR = "Bill Kayser"
9
+ EMAIL = "bkayser@newrelic.com"
10
+ HOMEPAGE = "http://www.github.com/newrelic/tendersync"
11
+ SUMMARY = "Utility for syncing and indexing files from ENTP's Tender site."
12
+ DESCRIPTION = <<-EOF
13
+ Tendersync is a utility for syncing files from ENTP's Tender site for managing customer facing documentation. It can be used to pull and push documents to a local repository as well as create indexes for each documentation section.
14
+ EOF
15
+
16
+ # Generate all the Rake tasks
17
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
18
+ Echoe.new(GEM_NAME, Tendersync::VERSION) do |p|
19
+ p.author = AUTHOR
20
+ p.email = EMAIL
21
+ p.summary = SUMMARY
22
+ p.url = HOMEPAGE
23
+ p.project = 'newrelic'
24
+ p.description = DESCRIPTION
25
+ p.version = Tendersync::VERSION
26
+ p.need_tar_gz = false
27
+ p.need_gem = true
28
+ p.bin_files = 'bin/tendersync'
29
+ p.runtime_dependencies = [
30
+ ['mechanize','>= 0.9.3'],
31
+ ]
32
+ p.development_dependencies = [
33
+ ['newgem', ">= #{::Newgem::VERSION}"]
34
+ ]
35
+ p.ignore_pattern = %w[docs/** general/** troubleshooting/**]
36
+ p.clean_pattern |= %w[**/.DS_Store tmp *.log]
37
+ end
38
+
39
+ Dir['tasks/**/*.rake'].each { |t| load t }
40
+
data/bin/tendersync ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2009-6-11.
4
+ # Copyright (c) 2009. All rights reserved.
5
+
6
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/tendersync")
7
+
8
+ require "tendersync/runner"
9
+
10
+ begin
11
+ Tendersync::Runner.new(ARGV.dup).run
12
+ rescue Tendersync::Runner::Error => e
13
+ puts e.message
14
+ end
@@ -0,0 +1,2 @@
1
+ host: unknown@rubyforge.org
2
+ remote_dir: /var/www/gforge-projects/tendersync
@@ -0,0 +1,218 @@
1
+ require 'fileutils'
2
+ require 'set'
3
+ require 'yaml'
4
+
5
+ class Tendersync::Document
6
+ Properties = [:section, :document_id, :title, :permalink, :keywords, :body]
7
+ attr_accessor *Properties
8
+
9
+ NUM_DASHES = 28 # the number of dashes in keyword fields
10
+
11
+ class TOCEntry
12
+ attr_reader :name, :link, :level
13
+ attr_accessor :parent
14
+ def initialize name, link=nil, level=nil
15
+ @name = name
16
+ @link = link if link
17
+ @level = level if level
18
+ end
19
+ def children
20
+ @children ||= []
21
+ end
22
+ # Write this element and all children, recursively as bullet lists with links
23
+ def write_entries(io, depth=1, indent = 0, doc_link = parent.link)
24
+ io.write " " * 4 * indent # indentation
25
+ io.write "* " # bullet
26
+ if link
27
+ io.puts "[#{name}](#{doc_link}##{self.link})"
28
+ else
29
+ io.puts name
30
+ end
31
+ children.each { | child | child.write_entries(io, depth-1, indent+1, doc_link)} unless depth == 1
32
+ end
33
+ def add child
34
+ if !parent || child.level > self.level
35
+ children << child
36
+ child.parent = self
37
+ else
38
+ parent.add(child)
39
+ end
40
+ child
41
+ end
42
+ end
43
+
44
+ class Group < TOCEntry
45
+ attr_reader :title_regex
46
+ def initialize(name, title_regex=//)
47
+ super(name, nil, nil)
48
+ @title_regex = title_regex
49
+ end
50
+ Default = Group.new('Other')
51
+
52
+ end
53
+
54
+ def initialize(values={})
55
+ values.each do | prop, value |
56
+ self.send "#{prop}=", value
57
+ end
58
+ end
59
+ #
60
+ # Documents can be read from / written to a file
61
+ #
62
+ def to_s
63
+ io = StringIO.new
64
+ Properties.each do |field|
65
+ next unless value = self.send(field)
66
+ io.write "-" * NUM_DASHES
67
+ io.write " #{field} "
68
+ io.write "-" * NUM_DASHES
69
+ io.puts
70
+ io.puts value
71
+ end
72
+ io.string
73
+ end
74
+
75
+ def save
76
+ FileUtils.mkdir_p section
77
+ File.open("#{section}/#{permalink}",'w') { |f| f.print self }
78
+ self
79
+ end
80
+
81
+ def self.load(section, io)
82
+ values = { :section => section }
83
+ key = data = nil
84
+ while line = io.gets
85
+ line.chomp!
86
+ if line =~ /^----+ (.+) -----+$/
87
+ values[key] = data.join("\n") if data
88
+ key = $1.intern
89
+ data = []
90
+ else
91
+ raise "keyword line not recognized: #{line}" unless data
92
+ data << line
93
+ end
94
+ end
95
+ values[key] = data.join("\n") if key
96
+ new values
97
+ end
98
+
99
+ def self.read_from_file(file_name)
100
+ section = file_name.split('/')[-2]
101
+ if !File.exists? file_name
102
+ raise Tendersync::Runner::Error, "Cannot read #{file_name}"
103
+ end
104
+ File.open(file_name) { |f| self.load(section, f) }
105
+ end
106
+
107
+ #
108
+ # Can be scraped from a form
109
+ #
110
+ def self.from_form(section,form)
111
+ values = {
112
+ :document_id => form.action[%r{/faqs/(\d+)/edit},1],
113
+ :section => section
114
+ }
115
+ form.fields.each { |tf|
116
+ if field_name = tf.name[/faq\[(.*)\]/,1]
117
+ value = tf.value.map { |line| line.chomp }.join("\n")
118
+ values[field_name.intern] = value
119
+ end
120
+ }
121
+ new(values)
122
+ end
123
+ def to_form(form)
124
+ form.fields.each { |tf|
125
+ if field_name = tf.name[/faq\[(.*)\]/,1] and self.send(field_name.intern)
126
+ lines = []
127
+ self.send(field_name.intern).each_line {|line| lines << line.chomp }
128
+ tf.value = lines.join("\r\n")
129
+ end
130
+ }
131
+ end
132
+
133
+ def self.each(section)
134
+ Dir.glob("#{section}/*").each { |f| yield Tendersync::Document.read_from_file(f) }
135
+ end
136
+
137
+ def self.index_for(section_id, section_name)
138
+ new(:section => section_id,
139
+ :title => "#{section_name} Table of Contents",
140
+ :permalink => "#{section_id}-table-of-contents",
141
+ :keywords => "toc index")
142
+ end
143
+
144
+ # Update this document body with an index of all the documents in
145
+ # this section. Underneath the TOC entry for a document will be sub
146
+ # entries for each named A element.
147
+ #
148
+ # group_map is an associative array of /regex/ to "Title String" of
149
+ # a group of documents. It is used to divide up documents into
150
+ # groups within the table of contents. A document is placed in a
151
+ # TOC group based on the first regex it matches in the group map.
152
+ #
153
+ # If group_map is empty then headings will be sorted alphabetically
154
+ # and not grouped.
155
+ #
156
+ # depth s the number of nested levels to descend into a document.
157
+ def refresh_index(groups=[], depth=2)
158
+ generate_index(create_toc(groups), depth)
159
+ end
160
+
161
+ private
162
+
163
+ def create_toc(groups)
164
+ groups += groups + [Group::Default] # array of groups
165
+ link_root = {}
166
+ self.class.each(section) do |document|
167
+ next if document.permalink =~ /-table-of-contents$/
168
+ puts "indexing #{document.permalink}..."
169
+ title = document.title
170
+ group = groups.detect { | g | title =~ g.title_regex }
171
+ doc_entry = TOCEntry.new title, document.permalink, 0
172
+ group.add doc_entry
173
+ last = doc_entry
174
+ link = nil
175
+ document.body.scan(%r{<a name=(.*?)>|^(#+)\s*(.*?)\s*$}i) do
176
+ name = $1
177
+ heading_level = $2 && $2.length
178
+ text = $3
179
+ if name
180
+ # Record the link name for the next header
181
+ link = eval(name)
182
+ elsif heading_level == 1
183
+ last = last.add(TOCEntry.new(text, link, heading_level))
184
+ elsif heading_level >= 2 # level 2
185
+ last = last.add(TOCEntry.new(text, link, heading_level))
186
+ link = nil
187
+ end
188
+ end
189
+ end
190
+ groups
191
+ end
192
+
193
+ def generate_index(groups, depth)
194
+ groups.reject! { | group | group.children.empty? }
195
+ # Now go through each group
196
+ io = StringIO.new
197
+ io.puts
198
+ groups.each do | group |
199
+ # Show the group heading unless there is only one group
200
+ io.puts "## #{group.name}" unless groups.size == 1
201
+ group.children.each do | doc_entry |
202
+ doc_link = doc_entry.link
203
+ io.puts "### [#{doc_entry.name}](#{doc_link})"
204
+ doc_entry.children.each do | doc_section |
205
+ doc_section.write_entries(io, depth)
206
+ end
207
+ io.puts
208
+ end
209
+ end
210
+ if $dry_run
211
+ puts io.string
212
+ else
213
+ self.body = io.string
214
+ end
215
+ end
216
+
217
+ end
218
+
@@ -0,0 +1,211 @@
1
+ require 'rubygems'
2
+ require 'optparse'
3
+ require 'tendersync/session'
4
+ require 'tendersync/document'
5
+ require 'mechanize'
6
+ require 'yaml'
7
+
8
+ class Tendersync::Runner
9
+ class Error < StandardError; end
10
+
11
+ def initialize argv
12
+ @dry_run = false
13
+ @sections = []
14
+ @groups = []
15
+ settings['groups'] ||= []
16
+ @parser = OptionParser.new do |op|
17
+ op.banner += " command\n"
18
+ op.on('-n', "dry run" ) { @dry_run = true }
19
+ op.on('-s', '--section', '=SECTION', String, "section, specify multiple separately" ) { |s| @sections << s }
20
+ op.on('-u', '--username','=EMAIL', String, "* login e-mail" ) {|str| settings['username'] = str }
21
+ op.on('-p', '--password','=PASS', String, "* password" ) {|str| settings['password'] = str }
22
+ op.on( '--docurl', '=URL', String, "* tender site URL" ) { |dir| settings['docurl'] = dir }
23
+ op.separator ""
24
+ op.separator "Indexing Options:"
25
+ op.on('-g', '--group', '=TITLE;regex', String, "*map of regex to group title for TOC groups") do | g |
26
+ pair = g.split(';')
27
+ settings['groups'] << [pair.first, pair.last]
28
+ end
29
+ op.on('-d', '--depth', '=DEPTH', String, "*Number of levels to descend into a document being indexed") { | g | settings['depth'] = g.to_i }
30
+
31
+
32
+ %Q{
33
+ * saved in .tendersync file for subsequent default
34
+
35
+ Commands:
36
+
37
+ pull [URL, URL...] -- download documents from tender; specify sections with -s, a page URL, or
38
+ nothing to download all documents
39
+ index -- create a master index of each section, writing to section/file; specify
40
+ the sections with -s options; you can organize the TOC into groups by
41
+ mapping document titles to groups via a regular expression with -g options
42
+ ls -- list files in specified session
43
+ post PATTERN -- post the matching documents to tender; use /regexp/ or glob
44
+ irb -- drops you into IRB with a tender session & related classes (for hacking/
45
+ one-time tasks). Programmers only.
46
+ create PERMALINK -- create a new tender document with the specified permalink in the section
47
+ specified by --section=... (must be only one.)
48
+
49
+ }.split(/\n/).each {|line| op.separator line.chomp }
50
+ end
51
+
52
+ begin
53
+ @command,*@args = *@parser.parse(argv)
54
+ rescue OptionParser::InvalidOption => e
55
+ raise Error, e.message
56
+ end
57
+
58
+ @username = settings['username']
59
+ @password = settings['password']
60
+ @dochome = settings['docurl'] && settings['docurl'] =~ /^(http.*?)\/?$/ && $1
61
+ @root = settings['root']
62
+
63
+ case
64
+ when ! @username
65
+ raise Error, "Please enter a username and password. You only need to do this once."
66
+ when ! @password
67
+ raise Error, "Please enter a password. You only need to do this once."
68
+ when ! @dochome
69
+ raise Error, "Please enter a --docurl indicating the home page URL of your Tender docs.\n" +
70
+ "You only need to do this once."
71
+ else
72
+ settings.save!
73
+ end
74
+ end
75
+
76
+ def run
77
+ $session = Tendersync::Session.new @dochome, @username, @password
78
+ $dry_run = @dry_run
79
+ case @command || 'help'
80
+ when 'help'
81
+ raise Error, @parser.to_s
82
+ when *%w[pull post create irb ls index]
83
+ send @command
84
+ else
85
+ raise Error, "Unknown command: #{@command}\n\n#{@parser}"
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def ls
92
+ $session.ls *sections
93
+ end
94
+
95
+ def pull
96
+ if @args.size > 0
97
+ @args.each do |url|
98
+ section = url =~ /\/faqs\/([^\/]*)\// && $1
99
+ raise Error, "Invalid URI for document: #{url}" if section.nil?
100
+ doc = Document.from_form(section, $session.edit_page_for(url).form_with(:action => /edit/))
101
+ puts " #{doc.permalink}"
102
+ doc.save unless $dry_run
103
+ end
104
+ else
105
+ sections.each do |section|
106
+ puts "pulling #{section} docs ..."
107
+ $session.pull_from_tender(section)
108
+ end
109
+ end
110
+ end
111
+
112
+ def post
113
+ documents = @args.collect { |doc_name|
114
+ matches = if doc_name =~ %r{/}
115
+ Dir.glob(doc_name)
116
+ else
117
+ Dir.glob("#{@root}/{#{sections.join(',')}}/#{doc_name}*")
118
+ end
119
+ if matches.empty?
120
+ puts "No documents match #{doc_name}"
121
+ else
122
+ matches.collect { |match| Tendersync::Document.read_from_file(match) }
123
+ end
124
+ }.flatten.compact
125
+ documents.each { |document|
126
+ if @dry_run
127
+ puts "would post #{document.section}/#{document.permalink} to tender."
128
+ else
129
+ $session.post(document)
130
+ end
131
+ }
132
+ end
133
+ alias push post
134
+
135
+ def create
136
+ raise Error, "You must specify exactly one section to put the document in." if sections.length != 1
137
+ raise Error, "You must specify exactly one document permalink." if @args.length != 1
138
+ section,permalink = sections.first,@args.first
139
+ filename = "#{@root}/#{section}/#{permalink}"
140
+ text = File.read(filename) rescue ""
141
+ text = "Put Text Here" if text.strip.empty?
142
+ if $dry_run
143
+ puts "would create document #{permalink}\nin #{section} as #{filename}"
144
+ puts "\ntext:\n------------\n#{text}"
145
+ else
146
+ document = $session.create_document(section,permalink,text)
147
+ document.save
148
+ end
149
+ end
150
+
151
+ def irb
152
+ puts <<-EOF
153
+
154
+ Use $session to access the Tendersync::Session instance.
155
+ Use Tendersync::Document to manipulate documents local and remote.
156
+
157
+ Examples of crazy stuff you could try:
158
+
159
+ puts $session.all_sections.inspect
160
+
161
+ $session.pull_from_tender('troubleshooting')
162
+
163
+ $session.post(Tendersync::Document.index('docs').save)
164
+
165
+ Tendersync::Document.each { |d| puts d.body.split(/\W/).join("\\n") }
166
+
167
+ doc = Tendersync::Document.read_from_file("./docs/agent-api")
168
+ doc.body.gsub! /api/,"API"
169
+ doc.save
170
+
171
+ EOF
172
+ ARGV.clear
173
+ require 'irb'
174
+ require 'irb/completion'
175
+ $sections = sections
176
+ IRB.start
177
+ end
178
+ def index
179
+ groups = settings['groups'].map do |title,regex|
180
+ regex = eval(regex) if regex =~ %r{^/.*/[a-z]*$}
181
+ Tendersync::Document::Group.new title, Regexp.new(regex)
182
+ end
183
+ section_details = $session.all_sections
184
+ sections.each do |section|
185
+ doc = Tendersync::Document.index_for section, section_details[section]
186
+ puts "indexing #{section}: #{doc.section}/#{doc.permalink}"
187
+ doc.refresh_index groups, settings['depth'] || 2
188
+ doc.save
189
+ end
190
+ end
191
+ def sections
192
+ @sections = $session.all_sections.keys if @sections.empty?
193
+ @sections
194
+ end
195
+ def settings
196
+ case
197
+ when @settings
198
+ return @settings
199
+ when File.exists?(".tendersync")
200
+ File.open(".tendersync", "r") { |f| @settings = YAML.load(f) }
201
+ else
202
+ @settings = {}
203
+ end
204
+ def @settings.save!
205
+ File.open(".tendersync","w") do |f|
206
+ f.write(self.to_yaml)
207
+ end
208
+ end
209
+ @settings
210
+ end
211
+ end