milestoner 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a383b5ca439d263af13d3a99df04900c5cf9af6d
4
- data.tar.gz: 7bbeabe417fa74b1ea2de76f98ca4ad5cbc02468
3
+ metadata.gz: 2f8ac8304b27843a087593b12cf0ed45945701bc
4
+ data.tar.gz: 2fc0db25d0afba6db7131dc340d3404b478e19a2
5
5
  SHA512:
6
- metadata.gz: 203b4857ce7f978e63560a760ed56369853cad84405bfadb8b83e0b1f459023f495a44a0bd7b196ae4a17c14da44ab17cd6f6fe0b156b7d72047e442a28db62e
7
- data.tar.gz: 87add04071261e77f5964cf8202a914891228af308b3f90bf65c66ea94f585c3671b903a2a488dd6c1d6e92eb63e9003ec303dcaeb05c177625ac75637ac42b9
6
+ metadata.gz: 1eebacca1ca1eb38c801ffbd1ee35ad749e07a6f64394faa381385a325f83a11f6ad2fbdb533b313109e267775ce9fca4057f6eebf076929c205ead896cd57b4
7
+ data.tar.gz: bf566fa3250f24929aa519d647f64b2b77e47e0c6c235a79d70e68060d0bbf61d562bf91454988f187834d8e43c70cf9b77081240b1e226d7e12bdcad74bde83
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/README.md CHANGED
@@ -16,6 +16,8 @@ A tool for automating and releasing Git repository milestones.
16
16
  - [Requirements](#requirements)
17
17
  - [Setup](#setup)
18
18
  - [Usage](#usage)
19
+ - [Command Line Interface (CLI)](#command-line-interface-cli)
20
+ - [Customization](#customization)
19
21
  - [Tests](#tests)
20
22
  - [Versioning](#versioning)
21
23
  - [Code of Conduct](#code-of-conduct)
@@ -28,15 +30,19 @@ A tool for automating and releasing Git repository milestones.
28
30
 
29
31
  # Features
30
32
 
31
- - Provides [Semantic Versioning](http://semver.org) for Git repositories in the form of
32
- `v<major>.<minor>.</maintenance>` tags. Example: `v0.1.0`.
33
- - Provides optional security for signing tags using GPG signing key.
34
- - Automatically includes commits since last tag (or HEAD if no tags exist) within each tag message.
35
- - Groups commit messages by consistent prefixes in order defined: "Fixed", "Added", "Updated", "Removed", "Refactored".
36
- Otherwise, they are alphabetically sorted.
37
- - Alphabetically sorts commit messages within each prefix group.
38
- - Ensures duplicate commit messages are removed (if any).
39
- - Sanitizes commit messages by removing extra spaces and `[ci skip]` text.
33
+ - Ensures [Semantic Versioning](http://semver.org) of Git repository tags:
34
+ - Format: `v<major>.<minor>.</maintenance>`.
35
+ - Example: `v0.1.0`.
36
+ - Ensures Git commits since last tag (or since initialization of repository if no tags exist) are included within each
37
+ Git tag message.
38
+ - Ensures Git commit messages are grouped by prefix, in order defined, for each Git tag message (prefixes can be
39
+ customized): "Fixed", "Added", "Updated", "Removed", "Refactored".
40
+ - Ensures Git commit messages are alphabetically sorted within each Git tag message.
41
+ - Ensures duplicate Git commit messages are removed (if any) within each Git tag message.
42
+ - Ensures Git commit messages are sanitized by removing extra spaces and `[ci skip]` text within each Git tag message.
43
+ - Provides optional security for signing Git tags with [GnuPG](https://www.gnupg.org) signing key.
44
+
45
+ [![asciicast](https://asciinema.org/a/ecyzvq1uvy3mn6mrknd36f5b7.png)](https://asciinema.org/a/ecyzvq1uvy3mn6mrknd36f5b7)
40
46
 
41
47
  # Requirements
42
48
 
@@ -48,7 +54,7 @@ A tool for automating and releasing Git repository milestones.
48
54
 
49
55
  For a secure install, type the following (recommended):
50
56
 
51
- gem cert --add <(curl -Ls http://www.my-website.com/gem-public.pem)
57
+ gem cert --add <(curl -Ls https://www.alchemists.io/gem-public.pem)
52
58
  gem install milestoner --trust-policy MediumSecurity
53
59
 
54
60
  NOTE: A HighSecurity trust policy would be best but MediumSecurity enables signed gem verification while
@@ -60,20 +66,23 @@ For an insecure install, type the following (not recommended):
60
66
 
61
67
  # Usage
62
68
 
63
- From the command line, type: milestoner help
69
+ ## Command Line Interface (CLI)
70
+
71
+ From the command line, type: `milestoner help`
64
72
 
65
- milestoner -P, [--publish=PUBLISH] # Tag and push to remote repository.
66
- milestoner -c, [--commits] # Show tag message commits for next milestone.
73
+ milestoner -P, [--publish=PUBLISH] # Tag and push milestone to remote repository.
74
+ milestoner -c, [--commits] # Show commits for next milestone.
75
+ milestoner -e, [--edit] # Edit Milestoner settings in default editor.
67
76
  milestoner -h, [--help=HELP] # Show this message or get help for a command.
68
- milestoner -p, [--push] # Push tags to remote repository.
77
+ milestoner -p, [--push] # Push local tag to remote repository.
69
78
  milestoner -t, [--tag=TAG] # Tag local repository with new version.
70
- milestoner -v, [--version] # Show version.
79
+ milestoner -v, [--version] # Show Milestoner version.
71
80
 
72
- For tag options, type: milestoner help tag
81
+ For tag options, type: `milestoner help tag`
73
82
 
74
83
  -s, [--sign], [--no-sign] # Sign tag with GPG key.
75
84
 
76
- For publish options, type: milestoner help publish
85
+ For publish options, type: `milestoner help publish`
77
86
 
78
87
  -s, [--sign], [--no-sign] # Sign tag with GPG key.
79
88
 
@@ -81,6 +90,51 @@ When using Milestoner, the `--publish` command is intended to be the only comman
81
90
  release as it handles all of the steps necessary for tagging and pushing a new release. Should individual steps
82
91
  be needed, then the `--tag` and `--push` options are available.
83
92
 
93
+ ## Customization
94
+
95
+ Should the default settings not be desired, customization is allowed via the `.milestonerrc` file. The `.milestonerrc`
96
+ can be created at a global and/or local level. Example:
97
+
98
+ - Global: `~/.milestonerrc`
99
+ - Local: `<project repository root>/.milestonerrc`
100
+
101
+ Order of precedence for any setting is resolved as follows (with the first taking top priority):
102
+
103
+ 0. CLI option. Example: A version passed to either the `--tag` or `--publish` commands.
104
+ 0. Local project repository `.milestonerrc`.
105
+ 0. Global `~/.milestonerrc`.
106
+
107
+ Any setting provided to the CLI during runtime would trump a local/global setting and a local setting would trump a
108
+ global setting. The global setting is the weakest of all but great for situations where custom settings should be
109
+ applied to *all* projects. It is important to note that local settings completely trump any global settings -- there is
110
+ no inheritance when local *and* global settings exist at the same time.
111
+
112
+ The `.milestonerrc` uses the following default settings:
113
+
114
+ ---
115
+ :version: ""
116
+ :git_commit_prefixes:
117
+ - Fixed
118
+ - Added
119
+ - Updated
120
+ - Removed
121
+ - Refactored
122
+ :git_tag_sign: false
123
+
124
+ Each `.milestonerrc` setting can be configured as follows:
125
+
126
+ - `version`: By default it is left blank so you'll be prompted by the CLI. However, it can be nice to bump this version
127
+ prior to each release and have the current version checked into source code at a per project level. The version, if
128
+ set, will be used to tag the repository. If the version is a duplicate, an error will be thrown. When supplying a
129
+ version, use this format: `<major>.<minor>.</maintenance>`. Example: `0.1.0`.
130
+ - `git_commit_prefixes`: Should the default prefixes not be desired, you can define Git commit prefixes that match your
131
+ style. *NOTE: Prefix order is important with the first prefix defined taking precedence over the second and so forth.*
132
+ Special characters are allowed for prefixes but should be enclosed in quotes if used. To disable prefix usage
133
+ completely, use an empty array. Example: `:git_commit_prefixes: []`.
134
+ - `git_tag_sign`: Defaults to `false` but can be enabled by setting to `true`. When enabled, a Git tag will require GPG
135
+ signing for enhanced security and include a signed signature as part of the Git tag. This is useful for public
136
+ milestones where the author of a milestone can be verified to ensure milestone integrity/security.
137
+
84
138
  # Tests
85
139
 
86
140
  To test, run:
data/bin/milestoner CHANGED
@@ -4,5 +4,5 @@ require "milestoner"
4
4
  require "milestoner/cli"
5
5
  require "milestoner/identity"
6
6
 
7
- Process.setproctitle Milestoner::Identity.label_version
7
+ Process.setproctitle Milestoner::Identity.version_label
8
8
  Milestoner::CLI.start
data/lib/milestoner.rb CHANGED
@@ -1,4 +1,10 @@
1
+ require "milestoner/aids/git"
2
+ require "milestoner/errors/base"
3
+ require "milestoner/errors/configuration"
4
+ require "milestoner/errors/duplicate_tag"
5
+ require "milestoner/errors/git"
6
+ require "milestoner/errors/version"
7
+ require "milestoner/configuration"
1
8
  require "milestoner/identity"
2
- require "milestoner/exceptions"
3
9
  require "milestoner/pusher"
4
10
  require "milestoner/tagger"
@@ -0,0 +1,10 @@
1
+ module Milestoner
2
+ module Aids
3
+ # Augments an object with Git support.
4
+ module Git
5
+ def git_supported?
6
+ File.exist? File.join(Dir.pwd, ".git")
7
+ end
8
+ end
9
+ end
10
+ end
@@ -9,58 +9,63 @@ module Milestoner
9
9
  include Thor::Actions
10
10
  include ThorPlus::Actions
11
11
 
12
- package_name Milestoner::Identity.label
12
+ package_name Milestoner::Identity.version_label
13
13
 
14
14
  def initialize args = [], options = {}, config = {}
15
15
  super args, options, config
16
+ @configuration = Milestoner::Configuration.new Milestoner::Identity.file_name, defaults: defaults
17
+ @tagger = Milestoner::Tagger.new configuration.settings[:version],
18
+ commit_prefixes: configuration.settings[:git_commit_prefixes]
19
+ @pusher = Milestoner::Pusher.new
16
20
  end
17
21
 
18
- desc "-c, [--commits]", "Show tag message commits for next milestone."
22
+ desc "-c, [--commits]", "Show commits for next milestone."
19
23
  map %w(-c --commits) => :commits
20
- def commits version
21
- tagger = Milestoner::Tagger.new version
22
- say "Milestone #{version} Commits:"
24
+ def commits
23
25
  tagger.commit_list.each { |commit| say commit }
26
+ rescue Milestoner::Errors::Base => base_error
27
+ error base_error.message
24
28
  end
25
29
 
26
30
  desc "-t, [--tag=TAG]", "Tag local repository with new version."
27
31
  map %w(-t --tag) => :tag
28
32
  method_option :sign, aliases: "-s", desc: "Sign tag with GPG key.", type: :boolean, default: false
29
- def tag version
30
- tagger = Milestoner::Tagger.new version
31
- tagger.create sign: options[:sign]
33
+ def tag version = configuration.settings[:version]
34
+ tagger.create version, sign: sign_tag?(options[:sign])
32
35
  say "Repository tagged: #{tagger.version_label}."
33
- rescue Milestoner::VersionError => version_error
34
- error version_error.message
35
- rescue Milestoner::DuplicateTagError => tag_error
36
- error tag_error.message
36
+ rescue Milestoner::Errors::Base => base_error
37
+ error base_error.message
37
38
  end
38
39
 
39
- desc "-p, [--push]", "Push tags to remote repository."
40
+ desc "-p, [--push]", "Push local tag to remote repository."
40
41
  map %w(-p --push) => :push
41
42
  def push
42
- pusher = Milestoner::Pusher.new
43
43
  pusher.push
44
44
  info "Tags pushed to remote repository."
45
+ rescue Milestoner::Errors::Base => base_error
46
+ error base_error.message
45
47
  end
46
48
 
47
- desc "-P, [--publish=PUBLISH]", "Tag and push to remote repository."
49
+ desc "-P, [--publish=PUBLISH]", "Tag and push milestone to remote repository."
48
50
  map %w(-P --publish) => :publish
49
51
  method_option :sign, aliases: "-s", desc: "Sign tag with GPG key.", type: :boolean, default: false
50
- def publish version
51
- tagger = Milestoner::Tagger.new version
52
- pusher = Milestoner::Pusher.new
53
- tag_and_push tagger, pusher, options
54
- rescue Milestoner::VersionError => version_error
55
- error version_error.message
56
- rescue Milestoner::DuplicateTagError => tag_error
57
- error tag_error.message
52
+ def publish version = configuration.settings[:version]
53
+ tag_and_push version, options
54
+ rescue Milestoner::Errors::Base => base_error
55
+ error base_error.message
58
56
  end
59
57
 
60
- desc "-v, [--version]", "Show version."
58
+ desc "-e, [--edit]", "Edit #{Milestoner::Identity.label} settings in default editor."
59
+ map %w(-e --edit) => :edit
60
+ def edit
61
+ info "Editing: #{configuration.computed_file_path}..."
62
+ `#{editor} #{configuration.computed_file_path}`
63
+ end
64
+
65
+ desc "-v, [--version]", "Show #{Milestoner::Identity.label} version."
61
66
  map %w(-v --version) => :version
62
67
  def version
63
- say Milestoner::Identity.label_version
68
+ say Milestoner::Identity.version_label
64
69
  end
65
70
 
66
71
  desc "-h, [--help=HELP]", "Show this message or get help for a command."
@@ -71,10 +76,24 @@ module Milestoner
71
76
 
72
77
  private
73
78
 
74
- def tag_and_push tagger, pusher, options
75
- if tagger.create(sign: options[:sign]) && pusher.push
76
- say "Repository tagged and pushed: #{tagger.version_label}."
77
- say "Milestone published!"
79
+ attr_reader :configuration, :tagger, :pusher
80
+
81
+ def defaults
82
+ {
83
+ version: "",
84
+ git_commit_prefixes: %w(Fixed Added Updated Removed Refactored),
85
+ git_tag_sign: false
86
+ }
87
+ end
88
+
89
+ def sign_tag? sign
90
+ sign | configuration.settings[:git_tag_sign]
91
+ end
92
+
93
+ def tag_and_push version, options
94
+ if tagger.create(version, sign: sign_tag?(options[:sign])) && pusher.push
95
+ info "Repository tagged and pushed: #{tagger.version_label}."
96
+ info "Milestone published!"
78
97
  else
79
98
  tagger.destroy
80
99
  end
@@ -0,0 +1,36 @@
1
+ module Milestoner
2
+ # Default configuration for gem with support for custom settings.
3
+ class Configuration
4
+ def initialize file_name = ".unknownrc", defaults: {}
5
+ @file_name = file_name
6
+ @defaults = defaults
7
+ end
8
+
9
+ def local_file_path
10
+ File.join Dir.pwd, file_name
11
+ end
12
+
13
+ def global_file_path
14
+ File.join ENV["HOME"], file_name
15
+ end
16
+
17
+ def computed_file_path
18
+ File.exist?(local_file_path) ? local_file_path : global_file_path
19
+ end
20
+
21
+ def settings
22
+ defaults.merge(load_settings).reject { |_, value| value.nil? }
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :file_name, :defaults
28
+
29
+ def load_settings
30
+ yaml = YAML.load_file computed_file_path
31
+ yaml.is_a?(Hash) ? yaml : {}
32
+ rescue
33
+ defaults
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ module Milestoner
2
+ module Errors
3
+ # The base class for all Milestoner related errors.
4
+ class Base < StandardError
5
+ def initialize message = "Invalid Milestoner action."
6
+ super message
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Milestoner
2
+ module Errors
3
+ # Raised for invalid gem configuration.
4
+ class Configuration < Base
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Milestoner
2
+ module Errors
3
+ # Raised for duplicate tags.
4
+ class DuplicateTag < Base
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ module Milestoner
2
+ module Errors
3
+ # Raised for projects not initialized as Git repositories.
4
+ class Git < Base
5
+ def initialize message = "Invalid Git repository."
6
+ super message
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Milestoner
2
+ module Errors
3
+ # Raised for invalid version formats.
4
+ class Version < Base
5
+ end
6
+ end
7
+ end
@@ -10,11 +10,15 @@ module Milestoner
10
10
  end
11
11
 
12
12
  def self.version
13
- "0.3.0"
13
+ "0.4.0"
14
14
  end
15
15
 
16
- def self.label_version
16
+ def self.version_label
17
17
  [label, version].join " "
18
18
  end
19
+
20
+ def self.file_name
21
+ ".#{name}rc"
22
+ end
19
23
  end
20
24
  end
@@ -1,11 +1,14 @@
1
1
  module Milestoner
2
2
  # Handles publishing of Git tags to remote repository.
3
3
  class Pusher
4
+ include Aids::Git
5
+
4
6
  def initialize kernel: Kernel
5
7
  @kernel = kernel
6
8
  end
7
9
 
8
10
  def push
11
+ fail(Errors::Git) unless git_supported?
9
12
  kernel.system "git push --tags"
10
13
  end
11
14
 
@@ -1,42 +1,45 @@
1
1
  module Milestoner
2
2
  # Handles the tagging of a project repository.
3
3
  class Tagger
4
- attr_reader :version
4
+ include Aids::Git
5
5
 
6
- def self.commit_prefixes
7
- %w(Fixed Added Updated Removed Refactored) # Order is important for controlling the sort.
8
- end
9
-
10
- def self.commit_prefix_regex
11
- /\A(#{commit_prefixes.join "|"})/
12
- end
6
+ attr_reader :version_number, :commit_prefixes
13
7
 
14
8
  def self.version_regex
15
9
  /\A\d{1}\.\d{1}\.\d{1}\z/
16
10
  end
17
11
 
18
- def initialize version
19
- @version = validate_version version
12
+ def initialize version = nil, commit_prefixes: []
13
+ @version_number = version
14
+ @commit_prefixes = commit_prefixes
20
15
  end
21
16
 
22
17
  def version_label
23
- "v#{version}"
18
+ "v#{version_number}"
24
19
  end
25
20
 
26
21
  def version_message
27
- "Version #{version}."
22
+ "Version #{version_number}."
23
+ end
24
+
25
+ def commit_prefix_regex
26
+ return // if commit_prefixes.empty?
27
+ Regexp.union commit_prefixes
28
28
  end
29
29
 
30
30
  def tagged?
31
+ fail(Errors::Git) unless git_supported?
31
32
  response = `git tag`
32
33
  !(response.nil? || response.empty?)
33
34
  end
34
35
 
35
36
  def duplicate?
37
+ fail(Errors::Git) unless git_supported?
36
38
  system "git rev-parse #{version_label} > /dev/null 2>&1"
37
39
  end
38
40
 
39
41
  def commits
42
+ fail(Errors::Git) unless git_supported?
40
43
  groups = build_commit_prefix_groups
41
44
  group_by_commit_prefix! groups
42
45
  sort_by_commit_prefix! groups
@@ -47,20 +50,15 @@ module Milestoner
47
50
  commits.map { |commit| "- #{commit}" }
48
51
  end
49
52
 
50
- def create sign: false
51
- fail(DuplicateTagError, "Duplicate tag exists: #{version_label}.") if duplicate?
52
-
53
- begin
54
- message_file = Tempfile.new Milestoner::Identity.name
55
- File.open(message_file, "w") { |file| file.write tag_message }
56
- `git tag #{tag_options message_file, sign: sign}`
57
- ensure
58
- message_file.close
59
- message_file.unlink
60
- end
53
+ def create version = version_number, sign: false
54
+ fail(Errors::Git) unless git_supported?
55
+ @version_number = validate_version version
56
+ fail(Errors::DuplicateTag, "Duplicate tag exists: #{version_label}.") if duplicate?
57
+ create_tag sign: sign
61
58
  end
62
59
 
63
60
  def destroy
61
+ fail(Errors::Git) unless git_supported?
64
62
  `git tag --delete #{version_label}`
65
63
  end
66
64
 
@@ -68,7 +66,7 @@ module Milestoner
68
66
 
69
67
  def validate_version version
70
68
  message = "Invalid version: #{version}. Use: <major>.<minor>.<maintenance>."
71
- fail(VersionError, message) unless version.match(self.class.version_regex)
69
+ fail(Errors::Version, message) unless version.match(self.class.version_regex)
72
70
  version
73
71
  end
74
72
 
@@ -81,7 +79,7 @@ module Milestoner
81
79
  end
82
80
 
83
81
  def build_commit_prefix_groups
84
- groups = self.class.commit_prefixes.map.with_object({}) { |prefix, group| group.merge! prefix => [] }
82
+ groups = commit_prefixes.map.with_object({}) { |prefix, group| group.merge! prefix => [] }
85
83
  groups.merge! "Other" => []
86
84
  end
87
85
 
@@ -91,7 +89,7 @@ module Milestoner
91
89
 
92
90
  def group_by_commit_prefix! groups = {}
93
91
  raw_commits.each do |commit|
94
- prefix = commit[self.class.commit_prefix_regex]
92
+ prefix = commit[commit_prefix_regex]
95
93
  key = groups.key?(prefix) ? prefix : "Other"
96
94
  groups[key] << sanitize_commit(commit)
97
95
  end
@@ -110,5 +108,14 @@ module Milestoner
110
108
  return options.gsub("--sign ", "") unless sign
111
109
  options
112
110
  end
111
+
112
+ def create_tag sign: false
113
+ message_file = Tempfile.new Milestoner::Identity.name
114
+ File.open(message_file, "w") { |file| file.write tag_message }
115
+ `git tag #{tag_options message_file, sign: sign}`
116
+ ensure
117
+ message_file.close
118
+ message_file.unlink
119
+ end
113
120
  end
114
121
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: milestoner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brooke Kuhlmann
@@ -30,7 +30,7 @@ cert_chain:
30
30
  aSif+qBc6oHD7EQWPF5cZkzkIURuwNwPBngZGxIKaMAgRhjGFXzUMAaq++r59cS9
31
31
  xTfQ4k6fglKEgpnLAXiKdo2c8Ym+X4rIKFfedQ==
32
32
  -----END CERTIFICATE-----
33
- date: 2015-09-09 00:00:00.000000000 Z
33
+ date: 2015-09-13 00:00:00.000000000 Z
34
34
  dependencies:
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: thor
@@ -312,8 +312,14 @@ files:
312
312
  - README.md
313
313
  - bin/milestoner
314
314
  - lib/milestoner.rb
315
+ - lib/milestoner/aids/git.rb
315
316
  - lib/milestoner/cli.rb
316
- - lib/milestoner/exceptions.rb
317
+ - lib/milestoner/configuration.rb
318
+ - lib/milestoner/errors/base.rb
319
+ - lib/milestoner/errors/configuration.rb
320
+ - lib/milestoner/errors/duplicate_tag.rb
321
+ - lib/milestoner/errors/git.rb
322
+ - lib/milestoner/errors/version.rb
317
323
  - lib/milestoner/identity.rb
318
324
  - lib/milestoner/pusher.rb
319
325
  - lib/milestoner/tagger.rb
metadata.gz.sig CHANGED
Binary file
@@ -1,9 +0,0 @@
1
- module Milestoner
2
- # Raised for invalid version formats.
3
- class VersionError < StandardError
4
- end
5
-
6
- # Raised for duplicate tags.
7
- class DuplicateTagError < StandardError
8
- end
9
- end