story_branch 1.0.0 → 2.2.0

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,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4196567f738d9eefe7e9b689bba4a082f3604d41c7e4b198f10295e3f867a0ba
4
- data.tar.gz: 4bd0f419c7fce7703c1b5df98d906137bfd6b3c53e400273c59caf8bea923d26
3
+ metadata.gz: 470bb59ffdc939bd91f9700a00f467ca2b68535d7a30740b95792cb6c43b37e3
4
+ data.tar.gz: 49ca86d91a3f963f4096b84a3192ebc9a8da547a6dc0bb44fc465c4ca5eb149b
5
5
  SHA512:
6
- metadata.gz: 60993c9099b5112018ea6949ad47901bc27349473296ba96590bed89b499d96eea9ad185623d66d16ba28216b71d8efcbbd91131e89cb51e17da370037416eca
7
- data.tar.gz: 0a5154411fdb3c53b5efc0e444af8383b9a197e241bb0ffbe0322c38ccf56d92ee5501e1f3b25026aa1b181b10c66736f9fce90fb25a1a3df1d0530defea7729
6
+ metadata.gz: 9627d3631e828b32bb5d064f0580d4b940c23dd6c19d809583be4d32e52298646978355316c009870c8d9fb05b8611137137374dfbb644c6554bb0d176f21c6a
7
+ data.tar.gz: cc087771f69a847b3822181c8156aec52515993aba41a4fb7c3af01f33961c8a32826eae0d242d931595cebf9c00c6a8f4301a8498e87bbeeb218bfcf69ef715
data/.circleci/config.yml CHANGED
@@ -1,15 +1,14 @@
1
- version: 2
2
-
3
- defaults: &defaults
4
- docker:
5
- - image: circleci/ruby:2.6.5-node
6
- working_directory: ~/repo
1
+ version: 2.1
7
2
 
8
3
  workflows:
9
4
  version: 2
10
5
  test-and-publish:
11
6
  jobs:
12
7
  - test:
8
+ matrix:
9
+ parameters:
10
+ ruby_version: ["ruby:2.6-buster", "ruby:2.7-buster", "ruby:3.0-buster"]
11
+
13
12
  filters:
14
13
  tags:
15
14
  # this enables circleci to trigger on tags
@@ -26,7 +25,12 @@ workflows:
26
25
 
27
26
  jobs:
28
27
  test:
29
- <<: *defaults
28
+ parameters:
29
+ ruby_version:
30
+ type: string
31
+ docker:
32
+ - image: circleci/<< parameters.ruby_version >>
33
+ working_directory: ~/repo
30
34
 
31
35
  steps:
32
36
  - checkout
@@ -38,22 +42,12 @@ jobs:
38
42
  chmod +x ./cc-test-reporter
39
43
  - run: ./cc-test-reporter before-build
40
44
 
41
- - restore_cache:
42
- keys:
43
- - v2-dependencies-{{ checksum "Gemfile.lock" }}
44
- - v2-dependencies-
45
-
46
45
  - run:
47
46
  name: install dependencies
48
47
  command: |
49
48
  gem install bundler
50
49
  bundle install --jobs=4 --retry=3 --path vendor/bundle
51
50
 
52
- - save_cache:
53
- paths:
54
- - ./vendor/bundle
55
- key: v2-dependencies-{{ checksum "Gemfile.lock" }}
56
-
57
51
  - run:
58
52
  name: run tests
59
53
  command: |
@@ -81,26 +75,19 @@ jobs:
81
75
  ./cc-test-reporter after-build -t simplecov --exit-code $?
82
76
 
83
77
  publish:
84
- <<: *defaults
78
+ docker:
79
+ - image: circleci/ruby:3.0-buster
80
+ working_directory: ~/repo
81
+
85
82
  steps:
86
83
  - checkout
87
84
 
88
- - restore_cache:
89
- keys:
90
- - v2-dependencies-{{ checksum "Gemfile.lock" }}
91
- - v2-dependencies-
92
-
93
85
  - run:
94
86
  name: install dependencies
95
87
  command: |
96
88
  gem install bundler
97
89
  bundle install --jobs=4 --retry=3 --path vendor/bundle
98
90
 
99
- - save_cache:
100
- paths:
101
- - ./vendor/bundle
102
- key: v2-dependencies-{{ checksum "Gemfile.lock" }}
103
-
104
91
  - run:
105
92
  name: Setup Rubygems
106
93
  command: bash .circleci/setup-rubygems.sh
@@ -8,12 +8,10 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
 
10
10
  steps:
11
- - uses: actions/checkout@v1
12
- - name: Set up Ruby 2.6
13
- uses: actions/setup-ruby@v1
14
- with:
15
- ruby-version: 2.6.x
11
+ - uses: actions/checkout@v3
12
+ - name: Set up Ruby 2.7
13
+ uses: ruby/setup-ruby@v1
16
14
  - name: Rubocop Linter
17
- uses: andrewmcodes/rubocop-linter-action@v0.1.2
15
+ uses: andrewmcodes/rubocop-linter-action@v3.3.0
18
16
  env:
19
17
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ story_branch-*.gem
4
4
  .byebug_history
5
5
  coverage
6
6
  tools/release*.*
7
+ Gemfile.lock
data/.story_branch.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- finish_tag: 'Resolves'
3
- tracker: 'github'
2
+ finish_tag: Resolves
3
+ tracker: github
4
4
  project_id:
5
5
  - story-branch/story_branch
data/Changelog.md CHANGED
@@ -1,3 +1,16 @@
1
+ # 2.2.0
2
+
3
+ Wed Aug 11 07:37:00 2022 +0800
4
+
5
+ - Add support for linear.app (#127)
6
+ - Adds dependency on story branch's graphql client
7
+ - Update README to match closer to current gem behavior
8
+
9
+ # 2.1.0
10
+ Wed Jul 20 18:30:00 2022 +0800
11
+
12
+ - Add configuration support to prepend branch name with a user prefix (#122)
13
+
1
14
  # 0.3.3
2
15
  Tue Jun 26 15:18:37 2018 +0800
3
16
 
data/README.md CHANGED
@@ -2,6 +2,29 @@
2
2
  [![CircleCI](https://circleci.com/gh/story-branch/story_branch/tree/master.svg?style=svg)](https://circleci.com/gh/story-branch/story_branch/tree/master)
3
3
  [![Maintainability](https://api.codeclimate.com/v1/badges/7dbd75908417656853d7/maintainability)](https://codeclimate.com/github/story-branch/story_branch/maintainability)
4
4
 
5
+ # Table of Contents
6
+
7
+ * [Installing](#installing)
8
+ * [Usage](#usage)
9
+ * [Commands available](#commands-available)
10
+ * [Commentary](#commentary)
11
+ * [Configuration](#configuration)
12
+ * [Configuring PivotalTracker](#configuring-pivotaltracker)
13
+ * [Configuring Github](#configuring-github)
14
+ * [Configuring JIRA](#configuring-jira)
15
+ * [Configuring LinearApp](#configuring-linearapp)
16
+ * [Available settings](#available-settings)
17
+ * [Issue placement](#issue-placement)
18
+ * [Branch username](#branch-username)
19
+ * [Finish tag](#finish-tag)
20
+ * [Creating a new branch following the naming convention](#creating-a-new-branch-following-the-naming-convention)
21
+ * [PivotalTracker specific commands](#pivotaltracker-specific-commands)
22
+ * [Migrating](#migrating)
23
+ * [Old configuration](#old-configuration)
24
+ * [Old commands](#old-commands)
25
+ * [Contributing](#contributing)
26
+
27
+
5
28
  # Story Branch
6
29
 
7
30
  Story branch is a CLI application that interacts with Pivotal Tracker, Github and Jira
@@ -46,25 +69,97 @@ Commands:
46
69
  story_branch version # story_branch gem version
47
70
  ```
48
71
 
49
- ## Settings
72
+ ## Commentary
73
+
74
+ `story_branch configure`: Step by step configuration of a new tracker for your project
75
+
76
+ ### Configuration
77
+
78
+ The configuration is split into two different files: a `.story_branch.yml` in the root folder
79
+ of the project where you're configuring the tool and a `.story_branch.yml` in user's home directory.
80
+
81
+ For the management of the home directory, story_branch relies on [XDG](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
82
+ specification, so if configured, it'll be installed under `~/.config` or whatever your machine
83
+ specifies.
84
+
85
+ The idea behind the two files is that the one in the root of the project should be committed to your
86
+ repository and defines basic tracker configuration settings to be shared across the contributors
87
+ to your repository. These configuration settings include the tracker type, project id in the tracker,
88
+ where you want the ticket number to be placed amongst others.
89
+
90
+ The file under your config directory is meant to be stored only locally as it will contain the api
91
+ keys needed for story branch to access your tracker. The story_branch file under your config directory
92
+ should not be published anywhere.
50
93
 
51
- Story branch has a command available that will help you creating the configurations
52
- for the projects, but essentially you'll be asked for the pivotal tracker project id and your api key.
94
+ #### Configuring PivotalTracker
95
+
96
+ When running the command `story_branch configure` you'll be asked 3 things:
97
+ 1. tracker - You should select Pivotal Tracker
98
+ 2. project id - This can be fetched from the PivotalTracker url. E.g in the url `https://www.pivotaltracker.com/n/projects/651417`, the project id would be `651417`
99
+ 3. api key - this is your personal api key. You can get that from [your profile page](https://www.pivotaltracker.com/profile)
100
+
101
+ #### Configuring Github
102
+
103
+ When running the command `story_branch configure` you'll be asked 3 things:
104
+ 1. tracker - You should select Github
105
+ 2. project id - This is the github repository name in the format `<owner>/<repo_name>`. E.g. `story-branch/story_branch`.
106
+ 3. api key - this is your personal api token. You can create one under your [developer profile tokens page](https://github.com/settings/tokens)
107
+
108
+ #### Configuring JIRA
109
+
110
+ The configuration for JIRA is slightly more complex as the endpoint changes according
111
+ to your project setup. You will need an API token, which you can create a new one in your [JIRA id management page](https://id.atlassian.com/manage/api-tokens)
112
+ 1. tracker - You should select JIRA
113
+ 2. JIRA's subdomain - you should type the JIRA's subdomain that you use to access in your browser. E.g I'd type perxtechnologies to access to <https://perxtechnologies.atlassian.net>
114
+ 3. JIRA's project key - this should match which project you want to fetch the issues from. E.g. PW is the key for my Project Whistler, so I'd type PW
115
+ 4. API key that you should have gotten in the first description step
116
+ 5. username used for login in the JIRA usually. If you use google email authentication, the username should be your email
117
+
118
+ #### Configuring LinearApp
119
+
120
+ When running the command `story_branch configure` you'll be asked 3 things:
121
+ 1. tracker - You should select LinearApp
122
+ 2. project id - This should be your team's id.
123
+ 3. api key - this is your personal api token. You can create one under your [account API settings](https://linear.app/settings/api)
124
+
125
+ #### Available settings
126
+
127
+ ##### Issue placement
128
+
129
+ On your local config you can add a line with `issue_placement: <Beginning|End>`.
130
+ Based on this configuration, when running `story_branch create`, the ticket id will be
131
+ used as prefix or suffix on the branch name.
132
+
133
+ E.g.
134
+ `issue_placement: Beginning`
53
135
 
54
- ### Configuring the project id
136
+ `story_branch create` will create a branch in the format: `<issue_number>-<issue_title>`
55
137
 
56
- The project id you can get it easily from the url when viewing the project.
57
- This value will be stored in the local configuration file that will be committed
58
- to the working repository
138
+ While
59
139
 
60
- ### Configuring the api key
140
+ `issue_placement: End`
61
141
 
62
- The api key you can get it from your account settings.
63
- This value will be stored in your global configuration file that typically is
64
- not shared with your co-workers in the repository. This way, each user will
65
- be properly identified in the tracker
142
+ `story_branch create` will create a branch in the format: `<issue_title>-<issue_number>`
66
143
 
67
- ### Configuring the finish tag
144
+ ##### Branch username
145
+
146
+ In some cases your workflow requires you to have an identifier prefixing the branch name.
147
+ You can configure that by setting the configuration `branch_username` under your project's
148
+ name in the global `story_branch.yml` file (`defaults to: ~/.config/.story_branch.yml`)
149
+
150
+ E.g.
151
+
152
+ ```
153
+ story-branch/story_branch:
154
+ api_key: my_fantastic_api_key
155
+ branch_username: rui
156
+ ```
157
+
158
+ Doing so, when running `story_branch create`, it will create a branch in the format: `rui/<issue_number>-<issue_title>`
159
+
160
+ Naturally, the issue number will be placed based on the issue placement setting
161
+
162
+ ##### Finish tag
68
163
 
69
164
  On your local config you can add a line with `finish_tag: <Some random word>`.
70
165
  This tag will be used in the commit message when running `story_branch finish`.
@@ -75,28 +170,11 @@ E.g.
75
170
  `story_branch finish` will make a commit with the message
76
171
  `[Resolves #12313] story title`
77
172
 
173
+ ### Creating a new branch following the naming convention
78
174
 
79
- ### .story_branch files
80
-
81
- When configuring story branch, it will create two .story_branch.yml files: one in
82
- your home folder (`~/`) and one in your project's root (`./`).
83
- The one in your home folder will be used to store the different project's configurations
84
- such as which api key to use. This is done so you don't need to commit your
85
- api key to the repository but still be able to use different keys in case you
86
- have different accounts.
87
-
88
- The one in your project root will keep a reference to the project configuration.
89
- For now, this reference is the project id. This file can be safely committed to
90
- the repository and shared amongst your co-workers.
91
-
92
- ## Commentary
93
-
94
- `story_branch create`: Creates a git branch with automatic reference to a
95
- Pivotal Tracker Story. It will get started stories from your active
96
- project. You can enter text and press TAB to search for a story
97
- name, or TAB to show the full list. It will then suggest an editable
98
- branch name. When the branch is created the `story_id` will
99
- be appended to it.
175
+ `story_branch create`: Creates a git branch with automatic reference to a tracker ticket.
176
+ The tickets/stories that will be fetched will depend on the project type. Once you choose the
177
+ ticket to work on, a new branch will be created based on the ticket title and id.
100
178
 
101
179
  e.g. `my-story-name-1234567`
102
180
 
@@ -107,6 +185,8 @@ e.g: `[Finishes #1234567] My story name`
107
185
  You must stage all changes (or stash them) first. Note the commit will not
108
186
  be pushed. Note: You'll be able to bail out of the commit.
109
187
 
188
+ ### PivotalTracker specific commands
189
+
110
190
  `story_branch start`: Start a story in Pivotal Tracker from the terminal.
111
191
  It'll get all un-started stories in your current project. You can
112
192
  enter text and press TAB to search for a story name, or TAB to show
@@ -117,31 +197,6 @@ It'll get all started stories in your current project. You can
117
197
  enter text and press TAB to search for a story name, or TAB to show
118
198
  the full list.
119
199
 
120
- ## Configuring PivotalTracker
121
-
122
- When running the command `story_branch configure` you'll be asked 3 things:
123
- 1. tracker - You should select Pivotal Tracker
124
- 2. project id - This can be fetched from the PivotalTracker url. E.g in the url `https://www.pivotaltracker.com/n/projects/651417`, the project id would be `651417`
125
- 3. api key - this is your personal api key. You can get that from [your profile page](https://www.pivotaltracker.com/profile)
126
-
127
- ## Configuring Github
128
-
129
- When running the command `story_branch configure` you'll be asked 3 things:
130
- 1. project id - This is the github repository name in the format `<owner>/<repo_name>`. E.g. `story-branch/story_branch`.
131
- 2. tracker - You should select Github
132
- 3. api key - this is your personal api token. You can create one under your
133
- [developer profile tokens page](https://github.com/settings/tokens)
134
-
135
- ## Configuring JIRA
136
-
137
- The configuration for JIRA is slightly more complex as the endpoint changes according
138
- to your project setup. You will need an API token, which you can create a new one in your [JIRA id management page](https://id.atlassian.com/manage/api-tokens)
139
- 1. tracker - You should select JIRA
140
- 2. JIRA's subdomain - you should type the JIRA's subdomain that you use to access in your browser. E.g I'd type perxtechnologies to access to https://perxtechnologies.atlassian.net
141
- 3. JIRA's project key - this should match which project you want to fetch the issues from. E.g. PW is the key for my Project Whistler, so I'd type PW
142
- 4. API key that you should have gotten in the first description step
143
- 5. username used for login in the JIRA usually. If you use google email authentication, the username should be your email
144
-
145
200
  ## Migrating
146
201
 
147
202
  ### Old configuration
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../config_manager'
4
4
  require_relative '../command'
5
+ require_relative '../constants'
5
6
  require 'tty-config'
6
7
  require 'tty-prompt'
7
8
 
@@ -14,6 +15,7 @@ module StoryBranch
14
15
  # and then add the project id specified by the user.
15
16
  class Configure < StoryBranch::Command
16
17
  def initialize(_options)
18
+ super()
17
19
  @new_config = ConfigManager.new
18
20
  end
19
21
 
@@ -72,12 +74,7 @@ module StoryBranch
72
74
  def tracker
73
75
  return @tracker if @tracker
74
76
 
75
- trackers = {
76
- 'Pivotal Tracker' => 'pivotal-tracker',
77
- 'Github' => 'github',
78
- 'JIRA' => 'jira'
79
- }
80
- @tracker = prompt.select('Which tracker are you using?', trackers)
77
+ @tracker = prompt.select('Which tracker are you using?', StoryBranch::AVAILABLE_TRACKERS)
81
78
  end
82
79
  end
83
80
  end
@@ -8,6 +8,7 @@ module StoryBranch
8
8
  # started stories in the tracker
9
9
  class Create < StoryBranch::Command
10
10
  def initialize(options)
11
+ super()
11
12
  @options = options
12
13
  end
13
14
 
@@ -7,6 +7,7 @@ module StoryBranch
7
7
  # Command to finish a story
8
8
  class Finish < StoryBranch::Command
9
9
  def initialize(options)
10
+ super()
10
11
  @options = options
11
12
  end
12
13
 
@@ -7,6 +7,7 @@ module StoryBranch
7
7
  # OpenIssue command is used to open the associated ticket in the browser
8
8
  class OpenIssue < StoryBranch::Command
9
9
  def initialize(options)
10
+ super()
10
11
  @options = options
11
12
  end
12
13
 
@@ -7,6 +7,7 @@ module StoryBranch
7
7
  # Command to start an estimated story
8
8
  class Start < StoryBranch::Command
9
9
  def initialize(options)
10
+ super()
10
11
  @options = options
11
12
  end
12
13
 
@@ -7,6 +7,7 @@ module StoryBranch
7
7
  # Command to unstart a previously started story
8
8
  class Unstart < StoryBranch::Command
9
9
  def initialize(options)
10
+ super()
10
11
  @options = options
11
12
  end
12
13
 
@@ -34,6 +34,10 @@ module StoryBranch
34
34
  @issue_placement ||= @config.fetch(:issue_placement, default: 'End')
35
35
  end
36
36
 
37
+ def branch_username
38
+ @branch_username ||= @config.fetch(project_key, :branch_username)
39
+ end
40
+
37
41
  def finish_tag
38
42
  @finish_tag ||= @config.fetch(project_key,
39
43
  :finish_tag, default: 'Finishes')
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StoryBranch
4
+ AVAILABLE_TRACKERS = {
5
+ 'Pivotal Tracker' => 'pivotal-tracker',
6
+ 'Github' => 'github',
7
+ 'JIRA' => 'jira',
8
+ 'LinearApp' => 'linearapp'
9
+ }.freeze
10
+
11
+ TRACKERS_CLASSES = {
12
+ 'pivotal-tracker' => 'StoryBranch::Pivotal::Tracker',
13
+ 'github' => 'StoryBranch::Github::Tracker',
14
+ 'jira' => 'StoryBranch::Jira::Tracker',
15
+ 'linearapp' => 'StoryBranch::LinearApp::Tracker'
16
+ }.freeze
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StoryBranch
4
+ # Base class to represent an issue/ticket
5
+ class IssueBase
6
+ attr_reader :title, :id, :html_url
7
+
8
+ def initialize(tracker_issue, project = nil)
9
+ @project = project
10
+ @story = tracker_issue
11
+ end
12
+
13
+ def to_s
14
+ "#{@id} - #{@title}"
15
+ end
16
+
17
+ def dashed_title
18
+ StoryBranch::StringUtils.normalised_branch_name @title
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../issue_base'
4
+
5
+ module StoryBranch
6
+ module LinearApp
7
+ # LinearApp Issue representation
8
+ class Issue < StoryBranch::IssueBase
9
+ # NOTE: project here represents the team_id only
10
+ def initialize(tracker_issue, project)
11
+ super
12
+ @title = tracker_issue['title']
13
+ @id = "#{@project}-#{tracker_issue['number']}"
14
+ @html_url = tracker_issue['url']
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'issue'
4
+
5
+ module StoryBranch
6
+ module LinearApp
7
+ # LinearApp groups tickets in teams, so this is the team representation in
8
+ # story branch. It's equivalent to a project
9
+ class Team
10
+ def initialize(team_id, client)
11
+ @team_id = team_id
12
+ @client = client
13
+ end
14
+
15
+ def stories(_options = {})
16
+ response = @client.get(graphql_query: graphql_query)
17
+ stories_json = response.data['viewer']['assignedIssues']['nodes']
18
+ stories_json.map { |story| Issue.new(story, @team_id) }
19
+ rescue StoryBranch::Graphql::Error => e
20
+ raise "Error while querying for tickets:\n#{e.message}"
21
+ end
22
+
23
+ private
24
+
25
+ def graphql_query # rubocop:disable Metrics/MethodLength
26
+ %(
27
+ query Issue {
28
+ viewer {
29
+ assignedIssues (filter: { team: { name: { eq: "#{@team_id}"} } }) {
30
+ nodes {
31
+ id
32
+ title
33
+ description
34
+ number
35
+ url
36
+ }
37
+ }
38
+ }
39
+ }
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'story_branch/graphql'
4
+ require_relative '../tracker_base'
5
+ require_relative 'team'
6
+
7
+ module StoryBranch
8
+ module LinearApp
9
+ # Linear App API wrapper for story branch tracker
10
+ class Tracker < StoryBranch::TrackerBase
11
+ API_URL = 'https://api.linear.app/'
12
+
13
+ def initialize(project_id:, api_key:, **)
14
+ super
15
+
16
+ # NOTE: project should be the representation of linear app team
17
+ @team_id = project_id
18
+ @api_key = api_key
19
+ end
20
+
21
+ def valid?
22
+ !@api_key.nil? && !@team_id.nil?
23
+ end
24
+
25
+ # TODO: This should probably be renamed to something more meaningful
26
+ # in the sense that it should be workable stories/issues
27
+ # which depend on the tracker's workflow. PivotalTracker they need to
28
+ # be started and estimated, while for Github they just need to be open
29
+ def stories
30
+ project.stories
31
+ end
32
+
33
+ def get_story_by_id(story_id)
34
+ project.stories(id: story_id).first
35
+ end
36
+
37
+ def client
38
+ @client ||= StoryBranch::Graphql::Client.new(api_url: API_URL, api_key: @api_key)
39
+ end
40
+
41
+ private
42
+
43
+ def configure_api
44
+ client
45
+ end
46
+
47
+ def project
48
+ return @project if @project
49
+ raise 'team must be set' unless @team_id
50
+
51
+ @project = Team.new(@team_id, client)
52
+ end
53
+ end
54
+ end
55
+ end