devlogs 0.3.1 → 1.1.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: 2023b8563770781f9708368975e70b713ec937df3331461096841e4f0fbd03a3
4
- data.tar.gz: b1e7b12bac7c07aede0fa9dc8e8daca29db668e256c7a73067bc1f56993cad58
3
+ metadata.gz: 710e8aea6a7ce1d312e3f94c36bb01c718358e94d46be5745956ffa66e902c77
4
+ data.tar.gz: f02110b5872d1cc117aad8b4e5cd5e5baabdb160336a9a300d96b16ad08aca5a
5
5
  SHA512:
6
- metadata.gz: 88c3a1909b19c35619abcaabd5815843ccb334362f2a1d5a8be7efb9f82fc64be9c43f51a1b0c76be012ef043b4a1b66bc0a4643ca2cb248ccafb3ee8a4b9fb3
7
- data.tar.gz: 925757b3d644479dc402f2c17a762a349a9eb8b5dda7a75ecb88ec71e9712933b1fd223d239164ce4a24b8e0864040ad51c1a5f78ccce42535b1599f3b5ebfd3
6
+ metadata.gz: 70776c55ba2df5ac5c14d96e6a773a30821e8ceb7fbcffeb9af65c005cd54cf04ec161dff61b8f0285f1324921ca1fb7d92aa05ce8ce48056423c5a7b3a1f28e
7
+ data.tar.gz: 371ede787d87a78a2de645734e8a0953467acb7a9e64a01c5d887057b894eefe3530649c2d482462d05119fc28fec9c5e6aeb37705944aab9641881c39ebb6ee
data/.gitignore CHANGED
@@ -10,4 +10,4 @@ __devlog
10
10
  *.gem
11
11
  __devlogs
12
12
  mirror
13
- _devlogs
13
+ .devlogs
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- devlogs (0.3.1)
4
+ devlogs (1.1.0)
5
5
  rsync (~> 1.0, >= 1.0.9)
6
6
  thor (~> 1.2.1)
7
7
  tty-prompt (~> 0.23.1)
data/README.md CHANGED
@@ -23,12 +23,12 @@ Or install it yourself as:
23
23
 
24
24
  ## Usage
25
25
  ### Initialize
26
- Inside your project initialize the `_devlogs` repository:
26
+ Inside your project initialize the `.devlogs` repository:
27
27
  ```bash
28
28
  $ devlogs init
29
29
  ```
30
30
 
31
- Follow the prompts to setup the project configuration located in `_devlogs/.devlogs.config`.
31
+ Follow the prompts to setup the project configuration located in the _default_ `.devlogs/.devlogs.config`. (You can optionally set where you want to initialize the repository via --dirpath)
32
32
 
33
33
  You can setup a mirror directory path in the configuration stage to sync changes to another directory on your machine, for example to Obsidian.md.
34
34
 
@@ -36,7 +36,7 @@ Example:
36
36
 
37
37
  ```
38
38
  myproject
39
- _devlogs
39
+ .devlogs
40
40
  >> content
41
41
  ```
42
42
 
@@ -47,11 +47,12 @@ obsidianvault
47
47
  >> content mirrored here
48
48
  ```
49
49
 
50
- ### Creating entries
51
- Once you are done for the day or session run the `entry` command:
50
+ ### Logs
51
+ #### Creating log entries
52
+ Once you are done for the day or session run the `new` command:
52
53
 
53
54
  ```bash
54
- devlogs entry
55
+ devlogs new
55
56
  ```
56
57
 
57
58
  Your editor will pop up and you can fill in cliff notes.
@@ -66,24 +67,60 @@ Your editor will pop up and you can fill in cliff notes.
66
67
 
67
68
  Save and if you set a mirror it will sync over!
68
69
 
69
- ### Retrieve previous entry
70
+ #### Retrieve previous entry
70
71
  You can use the `last` command to retrieve the most recent entry
71
72
 
72
73
  ```bash
73
74
  devlogs last
74
75
  ```
75
76
 
76
- The `--open` command will cause the entry to be opened in a new default editor.
77
+ #### List all log entires
78
+ You can use the `ls` command to retrieve the most recent entry
77
79
 
78
- ## Development
80
+ ```bash
81
+ devlogs ls
82
+ ```
83
+
84
+ ### Issues
85
+ Devlogs also allows you to manage issues locally as well. Devlogs creates a separate subdirectory in the `.devlogs` folder which will contain all issues. These files are also synced if the repository is mirrored.
86
+
87
+ #### Creating an Issue
88
+ You can create a new issue via `devlogs new_issue`. You will be prompted to provide some information and then your editor will open and you can fill in some details
89
+
90
+ ```bash
91
+ devlogs new_issue
92
+ ```
93
+
94
+ #### List all issues
95
+ You can use the `ls_issues` command to retrieve the most recent entry
79
96
 
97
+ ```bash
98
+ devlogs ls_issues
99
+ ```
100
+
101
+ ### Custom Templates
102
+ Devlogs initializes the log repository with two custom templates that you can edit freely `.log_template.erb.md` and `.issue_template.erb.md`. These are [Embedded Ruby Files](https://en.wikipedia.org/wiki/ERuby) meaning they can access certain variables.
103
+
104
+ #### Log Template
105
+ | Variable Name | Value |
106
+ | --- | --- |
107
+ | Time | The current date, hour and minute time |
108
+
109
+ #### Issue Template
110
+ | Variable Name | Value |
111
+ | --- | --- |
112
+ | Time | The current date, hour and minute time |
113
+ | Issue Title | The provided issue title input from the issue creation prompt |
114
+ | Description | The provided description input from the issue creation prompt |
115
+ | Reproduction Steps | The provided input for reproduction steps from the issue creation prompt |
116
+
117
+ ## Development
80
118
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
81
119
 
82
120
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
83
121
 
84
122
  ## Contributing
85
-
86
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/devlogs.
123
+ Bug reports and pull requests are welcome on GitHub at https://github.com/aquaflamingo/devlogs.
87
124
 
88
125
  ## License
89
126
 
data/lib/devlogs/cli.rb CHANGED
@@ -4,7 +4,8 @@ require_relative "version"
4
4
  require_relative "repository"
5
5
  require_relative "editor"
6
6
  require_relative "pager"
7
- require_relative "prompt_utils"
7
+ require_relative "helper/tty_prompt_helper"
8
+ require_relative "repository/initializer"
8
9
  require "thor"
9
10
 
10
11
  module Devlogs
@@ -12,7 +13,7 @@ module Devlogs
12
13
  # The CLI devlogs CLI
13
14
  #
14
15
  class CLI < Thor
15
- include PromptUtils
16
+ include TTYPromptHelper
16
17
 
17
18
  package_name "devlogs"
18
19
 
@@ -33,16 +34,19 @@ module Devlogs
33
34
  # Initializes a +devlogs+ repository with a configuration
34
35
  #
35
36
  desc "init", "Initialize a developer logs for project"
36
- method_options force: :boolean, alias: :string
37
+ method_options force: :boolean
38
+ method_options dirpath: :string
37
39
  def init
38
40
  puts "Creating devlogs repository"
39
41
 
40
- Repository::Initialize.run(
41
- { force: options.force? },
42
- File.join(".", "_devlogs")
42
+ Repository::Initializer.run(
43
+ {
44
+ force: options.force?,
45
+ dirpath: options.dirpath
46
+ }
43
47
  )
44
48
 
45
- puts "Created devlogs"
49
+ puts "Created devlogs repository"
46
50
  end
47
51
 
48
52
  #
@@ -60,18 +64,53 @@ module Devlogs
60
64
  puts File.read(last_entry)
61
65
  end
62
66
  end
67
+
68
+ # FIXME: Add logs sub command
63
69
  #
64
70
  # Creates a devlogs entry in the repository and syncs changes
65
71
  # to the mirrored directory if set
66
72
  #
67
- desc "entry", "Create a new devlogs entry" # [4]
68
- def entry
73
+ desc "new", "Create a new devlogs entry" # [4]
74
+ def new
69
75
  puts "Creating new entry..."
70
76
  repo.create
71
77
 
72
78
  repo.sync
73
79
  end
74
80
 
81
+ # FIXME: Add logs sub command
82
+ #
83
+ # Creates a devlogs entry in the repository and syncs changes
84
+ # to the mirrored directory if set
85
+ #
86
+ desc "new_issue", "Create a new devlogs entry" # [4]
87
+ def new_issue
88
+ repo.create_issue
89
+ repo.sync
90
+ end
91
+
92
+ #
93
+ # Lists repository issues
94
+ #
95
+ desc "ls_issues", "Lists the repository issues and allows you to select"
96
+ def ls_issues
97
+ issues = repo.ls_issues
98
+
99
+ if issues.empty?
100
+ puts "No issues present in this repository"
101
+ exit 0
102
+ end
103
+
104
+ # Use the file names as visible keys for the prompt
105
+ issue_names = issues.map { |e| File.basename(e) }
106
+
107
+ # Build the TTY:Prompt
108
+ result = build_select_prompt(data: issue_names, text: "Select an issue issue...")
109
+
110
+ # Open in paging program
111
+ Pager.open(issues[result])
112
+ end
113
+
75
114
  #
76
115
  # Lists repository logs
77
116
  #
@@ -79,6 +118,11 @@ module Devlogs
79
118
  def ls
80
119
  entries = repo.ls
81
120
 
121
+ if entries.empty?
122
+ puts "No logs present in this repository"
123
+ exit 0
124
+ end
125
+
82
126
  # Use the file names as visible keys for the prompt
83
127
  entry_names = entries.map { |e| File.basename(e) }
84
128
 
@@ -94,6 +138,7 @@ module Devlogs
94
138
  # Helper method for repository loading
95
139
  #
96
140
  def repo
141
+ # FIXME: Need to add in path specification here
97
142
  @repo ||= Repository.load
98
143
  end
99
144
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devlogs
4
+ def self.lib_root
5
+ File.join(File.dirname(__dir__), "devlogs")
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeHelper
4
+ # Example: 11-22-2022__13h43m
5
+ TIME_FORMAT_FILE_PREFIX = "%m-%d-%Y__%kh%Mm"
6
+
7
+ def current_time(format: TIME_FORMAT_FILE_PREFIX)
8
+ time = Time.new
9
+ time.strftime(format)
10
+ end
11
+ end
@@ -5,7 +5,7 @@ require "tty-prompt"
5
5
  #
6
6
  # Utility module for tty-prompt library
7
7
  #
8
- module PromptUtils
8
+ module TTYPromptHelper
9
9
  #
10
10
  # Builds a basic select prompt using the provided data
11
11
  #
@@ -22,4 +22,10 @@ module PromptUtils
22
22
  end
23
23
  end
24
24
  end
25
+
26
+ module Validator
27
+ def self.length_range(min: 0, max: 99)
28
+ ->(input) { input.size > min && input.size <= max }
29
+ end
30
+ end
25
31
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ #
6
+ # ErbTemplateRenderer is a base class for rendering arbitrary
7
+ # ERB templates.
8
+ #
9
+ class ErbTemplateRenderer
10
+ attr_reader :time
11
+
12
+ TIME_FORMAT_TEXT_ENTRY = "%m-%d-%Y %k:%M"
13
+
14
+ def initialize(template_file_path)
15
+ @time = Time.new.strftime(TIME_FORMAT_TEXT_ENTRY)
16
+ @template_file_path = template_file_path
17
+ end
18
+
19
+ #
20
+ # Runs the ERB rendering using the provided template file template_file_path
21
+ #
22
+ # @returns [String]
23
+ #
24
+ def render
25
+ erb = ERB.new(File.read(@template_file_path))
26
+ erb.result(get_binding)
27
+ end
28
+
29
+ # rubocop:disable
30
+ #
31
+ # For ERB
32
+ #
33
+ def get_binding
34
+ binding
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "erb_template_renderer"
4
+
5
+ #
6
+ # IssueTemplateRenderer captures issue information and
7
+ # renders it within a given ERB template
8
+ #
9
+ class IssueTemplateRenderer < ErbTemplateRenderer
10
+ attr_reader :title, :description, :reproduction
11
+
12
+ def initialize(template_file_path, info = {})
13
+ super(template_file_path)
14
+
15
+ @title = info[:display_title]
16
+ @description = info[:description]
17
+ @reproduction = info[:reproduction]
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "erb_template_renderer"
4
+
5
+ #
6
+ # LogTemplateRenderer captures log information and
7
+ # renders it within a given ERB template
8
+ #
9
+ class LogTemplateRenderer < ErbTemplateRenderer
10
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The repository's configuration values located in the yml file
4
+ class Repository
5
+ class Config
6
+ # FIXME: Need to figure out file path
7
+ attr_reader :name, :description, :mirror, :file_path, :template_file_path, :short_code
8
+
9
+ # Configuration associated with the Mirror
10
+ MirrorConfig = Struct.new(:use_mirror, :path, keyword_init: true)
11
+
12
+ def initialize(path, opts = {})
13
+ @file_path = path
14
+ @template_file_path = opts[:template_file_path]
15
+ @name = opts[:name]
16
+ @short_code = opts[:short_code]
17
+ @description = opts[:description]
18
+ @mirror = MirrorConfig.new(opts[:mirror])
19
+ end
20
+
21
+ # Returns whether or not the devlogs repository is configured to mirror
22
+ #
23
+ # @returns [Boolean]
24
+ def mirror?
25
+ @mirror.use_mirror
26
+ end
27
+
28
+ # Ensures no weird double trailing slash path values
29
+ def path(with_trailing: false)
30
+ if with_trailing
31
+ @file_path[-1] == "/" ? @path_value : @path_value + "/"
32
+ else
33
+ @file_path
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Repository
4
+ class ConfigBuilder
5
+ def initialize(dirpath)
6
+ @config_store = if dirpath
7
+ Repository::ConfigStore.new(dir: dirpath)
8
+ else
9
+ Repository::ConfigStore.new
10
+ end
11
+ end
12
+
13
+ def build
14
+ config_info = prompt_for_info
15
+
16
+ DraftConfig.new(@config_store, config_info)
17
+ end
18
+
19
+ private
20
+
21
+ # Creates an interactive prompt for user input
22
+ #
23
+ # @returns [Hash]
24
+ def prompt_for_info
25
+ prompt = TTY::Prompt.new
26
+
27
+ prompt.collect do |_p|
28
+ # Project name
29
+ key(:name).ask("What is the project name?") do |q|
30
+ q.required true
31
+ end
32
+
33
+ # Project description
34
+ key(:desc).ask("What is the project description?") do |q|
35
+ q.required true
36
+ end
37
+
38
+ # Project short code, e.g. RLP
39
+ key(:short_code).ask("What is the project short code (3 letters)?") do |q|
40
+ q.required true
41
+
42
+ q.validate ->(input) { input.size.positive? && input.size <= 3 }
43
+
44
+ q.messages[:valid?] = "Short code must be 3 letters or less"
45
+ end
46
+
47
+ key(:mirror) do
48
+ key(:use_mirror).ask("Do you want to mirror these logs?", convert: :boolean)
49
+ key(:path).ask("Path to mirror directory: ")
50
+ end
51
+ end
52
+ end
53
+
54
+ class DraftConfig
55
+ INFO_FILE_SUFFIX = "devlogs.info.md"
56
+ LOG_TEMPLATE_FILE_NAME = "__log_template.erb.md"
57
+ ISSUE_TEMPLATE_FILE_NAME = "__issue_template.erb.md"
58
+
59
+ def initialize(config_store, config_info)
60
+ @config_store = config_store
61
+ @config_info = config_info
62
+ end
63
+
64
+ #
65
+ # Initiates the write process of devlogs repository
66
+ #
67
+ # @param force [Boolean]
68
+ #
69
+ def save!(force: false)
70
+ exists = File.exist?(@config_store.file_path)
71
+
72
+ if exists && !force
73
+ puts "Log repository already exists in aborting..."
74
+ raise RuntimeError
75
+ end
76
+
77
+ create_config_store_dir
78
+ save_config_file
79
+ save_info_file
80
+ save_log_template_file
81
+
82
+ create_issue_dir
83
+ save_issue_template_file
84
+ save_data_file
85
+ end
86
+
87
+ private
88
+
89
+ #
90
+ # Creates the configuration store directory
91
+ #
92
+ def create_config_store_dir
93
+ # Create the draft_config directory
94
+ FileUtils.mkdir_p(@config_store.dir)
95
+ end
96
+
97
+ #
98
+ # Creates the issue directory
99
+ #
100
+ def create_issue_dir
101
+ # Create the draft_config directory
102
+ FileUtils.mkdir_p(@config_store.issue_dir_path)
103
+ end
104
+
105
+ #
106
+ # Saves the .devlogs.config.yml file
107
+ #
108
+ def save_config_file
109
+ # Create draft_config file
110
+ File.open(@config_store.file_path, "w") do |f|
111
+ f.write @config_info.to_yaml
112
+ end
113
+ end
114
+
115
+ #
116
+ # Saves the .info file
117
+ #
118
+ def save_info_file
119
+ # Replace spaces in project name with underscores
120
+ sanitized_project_name = @config_info[:name].gsub(/ /, "_").downcase
121
+
122
+ # Create the info file
123
+ info_file_name = "#{sanitized_project_name}.#{INFO_FILE_SUFFIX}"
124
+ info_file = File.join(@config_store.dir, info_file_name)
125
+
126
+ File.open(info_file, "w") do |f|
127
+ f.puts "# #{@config_info[:name]}"
128
+ f.puts (@config_info[:desc]).to_s
129
+ end
130
+ end
131
+
132
+ #
133
+ # Copies the log template to the config store directory
134
+ #
135
+ def save_log_template_file
136
+ # Copy the default template file inside the gem into the repository
137
+ default_log_template_path = get_template_path(LOG_TEMPLATE_FILE_NAME)
138
+
139
+ draft_config_log_template_file_path = File.join(@config_store.dir, Repository::ConfigStore::LOG_TEMPLATE_FILE)
140
+
141
+ FileUtils.cp(default_log_template_path, draft_config_log_template_file_path)
142
+ end
143
+
144
+ #
145
+ # Copies the log template to the config store directory
146
+ #
147
+ def save_issue_template_file
148
+ default_iss_template_path = get_template_path(ISSUE_TEMPLATE_FILE_NAME)
149
+
150
+ draft_config_iss_template_file_path = File.join(@config_store.dir, Repository::ConfigStore::ISSUE_TEMPLATE_FILE)
151
+
152
+ FileUtils.cp(default_iss_template_path, draft_config_iss_template_file_path)
153
+ end
154
+
155
+ #
156
+ # Creates a .devlogs.data.yml file
157
+ #
158
+ def save_data_file
159
+ data_file = File.join(@config_store.dir, Repository::ConfigStore::DATA_FILE)
160
+
161
+ #
162
+ # MARK: Default Data
163
+ #
164
+ data_info = {
165
+ issues: {
166
+ index: 1
167
+ },
168
+ logs: {},
169
+ repository: {}
170
+ }
171
+
172
+ File.open(data_file, "w") do |f|
173
+ f.puts data_info.to_yaml
174
+ end
175
+ end
176
+
177
+ # Gets the template file path embedded in the gem from the library root
178
+ #
179
+ # @returns [String]
180
+ #
181
+ def get_template_path(file_name)
182
+ File.join(Devlogs.lib_root, "templates", file_name)
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "rsync"
5
+
6
+ require_relative "config"
7
+
8
+ # A per repository configuration storage directory
9
+ class Repository
10
+ class ConfigStore
11
+ attr_reader :dir
12
+
13
+ CONFIG_FILE = ".devlogs.config.yml"
14
+ DATA_FILE = ".devlogs.data.yml"
15
+ ISSUE_TEMPLATE_FILE = ".issue_template.erb.md"
16
+ LOG_TEMPLATE_FILE = ".log_template.erb.md"
17
+
18
+ ISSUE_DIR = "issues"
19
+ DEFAULT_DIRECTORY_PATH = "."
20
+ DEFAULT_DIRECTORY_NAME = ".devlogs"
21
+
22
+ def initialize(dir: File.join(DEFAULT_DIRECTORY_PATH, DEFAULT_DIRECTORY_NAME))
23
+ @dir = dir
24
+ end
25
+
26
+ def values
27
+ @values ||= load_values_from_config_file
28
+ end
29
+
30
+ #
31
+ # Retrieves the data file
32
+ #
33
+ # @returns [String]
34
+ #
35
+ def data_file_path
36
+ File.join(@dir, DATA_FILE)
37
+ end
38
+
39
+ #
40
+ # Retrieves .devlogs.config.yml file path
41
+ #
42
+ # @returns [String]
43
+ #
44
+ def file_path
45
+ File.join(@dir, CONFIG_FILE)
46
+ end
47
+
48
+ #
49
+ # The template File
50
+ #
51
+ # @returns [String]
52
+ # FIXME: rename to log_template_file_path
53
+ def template_file_path
54
+ File.join(@dir, LOG_TEMPLATE_FILE)
55
+ end
56
+
57
+ #
58
+ # The issue template file path:
59
+ #
60
+ # @returns [String]
61
+ #
62
+ def issue_template_file_path
63
+ File.join(@dir, ISSUE_TEMPLATE_FILE)
64
+ end
65
+
66
+ #
67
+ # Issue directory path
68
+ #
69
+ # @returns [String]
70
+ #
71
+ def issue_dir_path
72
+ File.join(@dir, ISSUE_DIR)
73
+ end
74
+
75
+ class << self
76
+ #
77
+ # Initialization utility method
78
+ #
79
+ def load_from(path = File.join(DEFAULT_DIRECTORY_PATH, DEFAULT_DIRECTORY_NAME))
80
+ exists = File.exist?(path)
81
+
82
+ raise "no repository found #{path}" unless exists
83
+
84
+ new(dir: path)
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def load_values_from_config_file
91
+ yml = YAML.load_file(File.join(file_path))
92
+
93
+ Repository::Config.new(file_path, yml)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_store"
4
+ require_relative "config_builder"
5
+
6
+ # Initialize is an execution object which initializes a Repository on the
7
+ # filesystem
8
+ class Repository
9
+ class Initializer
10
+ # Creates a new devlogs repository at the provided path
11
+ def self.run(opts = {})
12
+ new_config = Repository::ConfigBuilder.new(opts[:dirpath])
13
+
14
+ draft = new_config.build
15
+
16
+ draft.save!(force: opts[:force])
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helper/time_helper"
4
+ require_relative "../helper/tty_prompt_helper"
5
+ require_relative "../render/issue_template_renderer"
6
+
7
+ #
8
+ # IssueManager is an abstraction class to orchestrate the internals
9
+ # of issue management and creation for a repository
10
+ #
11
+ class IssueManager
12
+ include TTYPromptHelper
13
+ include TimeHelper
14
+
15
+ VALID_DIRECTION = %i[asc desc].freeze
16
+ ISSUE_SEPARATOR = "__"
17
+
18
+ #
19
+ # @param [Repository::ConfigStore]
20
+ #
21
+ def initialize(repo_config_store)
22
+ @config_store = repo_config_store
23
+ end
24
+
25
+ #
26
+ # Lists the issue entries present in the repository
27
+ #
28
+ # @param direction [Symbol] ascending or descending
29
+ #
30
+ def list(direction = :desc)
31
+ raise ArgumentError, "Must be one of: " + VALID_DIRECTION unless VALID_DIRECTION.include?(direction.to_sym)
32
+
33
+ # Anything with the SHORTCODE- prefix
34
+ #
35
+ # i.e. RLP-1, RLP-2, et cetera
36
+ #
37
+ short_code_pattern = "#{config_values.short_code}-*"
38
+
39
+ #
40
+ # pattern: RLP-*
41
+ #
42
+ glob_pattern = File.join(@config_store.issue_dir_path, short_code_pattern)
43
+
44
+ Dir.glob(glob_pattern).sort_by do |fpath|
45
+ # i.e. [RLP-1, title_of_issue.md]
46
+ issue_tag, = File.basename(fpath).split(ISSUE_SEPARATOR)
47
+
48
+ # i.e. [RLP, 1]
49
+ _, issue_num = issue_tag.split("-")
50
+
51
+ if direction == :asc
52
+ issue_num.to_i
53
+ else
54
+ -1 * issue_num.to_i
55
+ end
56
+ end
57
+ end
58
+
59
+ #
60
+ # Adds a new entry to the repository
61
+ #
62
+ # @returns [String] entry file path
63
+ #
64
+ def create
65
+ info = issue_info_prompt
66
+
67
+ issue = compose_issue(info)
68
+
69
+ issue_file_path = File.join(@config_store.issue_dir_path, issue[:file_name])
70
+
71
+ template = IssueTemplateRenderer.new(@config_store.issue_template_file_path, issue)
72
+
73
+ unless File.exist?(issue_file_path)
74
+ # Add default boiler plate if the file does not exist yet
75
+ File.open(issue_file_path, "w") do |f|
76
+ f.write template.render
77
+ end
78
+
79
+ increment_issue_index!
80
+ end
81
+
82
+ issue_file_path
83
+ end
84
+
85
+ private
86
+
87
+ #
88
+ # Sanitizes and composes the content for display
89
+ #
90
+ # @returns [Hash]
91
+ def compose_issue(info = {})
92
+ # RLP-n
93
+ short_code_issue = compute_short_code
94
+
95
+ # RLP-1: User Validation Fails.md
96
+ display_title = "#{short_code_issue}: #{info[:title]}"
97
+
98
+ # rlp_1__user_validation_fails.md
99
+ file_name_title = snakify(info[:title])
100
+
101
+ issue_file_name = "#{short_code_issue}#{ISSUE_SEPARATOR}#{file_name_title}.md".downcase
102
+
103
+ {
104
+ display_title: display_title,
105
+ file_name: issue_file_name,
106
+ description: info[:description].join(""),
107
+ reproduction: info[:reproduction].join("")
108
+ }
109
+ end
110
+
111
+ #
112
+ # Increments the issue index in the data file by one
113
+ #
114
+ def increment_issue_index!
115
+ data = YAML.load_file(@config_store.data_file_path)
116
+
117
+ data[:issues][:index] += 1
118
+
119
+ File.open(@config_store.data_file_path, "w") do |f|
120
+ f.write data.to_yaml
121
+ end
122
+ end
123
+
124
+ #
125
+ # Gets TTY input for issue data
126
+ #
127
+ # @return [Hash]
128
+ #
129
+ def issue_info_prompt
130
+ prompt = TTY::Prompt.new
131
+
132
+ prompt.collect do
133
+ key(:title).ask("What is the issue title?") do |q|
134
+ q.required true
135
+ q.validate Validator.length_range(min: 0, max: 25)
136
+ q.messages[:valid?] = "Title cannot be empty and may be maximum 25 characters"
137
+ end
138
+
139
+ key(:description).multiline("Describe the issue: ") do |q|
140
+ q.default "There is an issue with..."
141
+ q.help "Press ctrl+d to end"
142
+ end
143
+
144
+ key(:reproduction).multiline("Describe the reproduction steps: ") do |q|
145
+ q.default "To reproduce the issue..."
146
+ q.help "Press ctrl+d to end"
147
+ end
148
+ end
149
+ end
150
+
151
+ #
152
+ # Convenience method for accessing config store values
153
+ #
154
+ # @returns [Repository::Config]
155
+ #
156
+ def config_values
157
+ @config_store.values
158
+ end
159
+
160
+ # Transforms a string with spaces or hyphens to
161
+ # an snake case String
162
+ #
163
+ # @param input [String]
164
+ # @returns [String]
165
+ #
166
+ def snakify(input)
167
+ input.gsub(/[ -]/, "_").downcase
168
+ end
169
+
170
+ #
171
+ # Reads the .data.yml file in the repository to
172
+ # get the latest issue index number
173
+ #
174
+ # @returns [String]
175
+ #
176
+ def compute_short_code
177
+ data = YAML.load_file(@config_store.data_file_path)
178
+
179
+ issue_index = data[:issues][:index]
180
+
181
+ "#{config_values.short_code}-#{issue_index}".upcase
182
+ end
183
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helper/time_helper"
4
+ require_relative "../render/log_template_renderer"
5
+
6
+ #
7
+ # LogManager is an abstraction class to orchestrate the internals
8
+ # of issue management and creation for a repository
9
+ #
10
+ class LogManager
11
+ include TimeHelper
12
+ LOG_FILE_SUFFIX = "log.md"
13
+ VALID_DIRECTION = %i[asc desc].freeze
14
+
15
+ def initialize(repo_config_store)
16
+ @config_store = repo_config_store
17
+ end
18
+
19
+ # Lists the log entries present in the repository
20
+ #
21
+ # @param direction [Symbol] ascending or descending
22
+ #
23
+ def list(direction = :desc)
24
+ raise ArgumentError, "Must be one of: " + VALID_DIRECTION unless VALID_DIRECTION.include?(direction.to_sym)
25
+
26
+ # Anything with the _log.md suffix
27
+ glob_pattern = File.join(@config_store.dir, "*_#{LOG_FILE_SUFFIX}")
28
+
29
+ Dir.glob(glob_pattern).sort_by do |fpath|
30
+ # The date is joined by two underscores to the suffix
31
+ date, = File.basename(fpath).split("_#{LOG_FILE_SUFFIX}")
32
+
33
+ time_ms = Time.strptime(date, TimeHelper::TIME_FORMAT_FILE_PREFIX).to_i
34
+
35
+ if direction == :asc
36
+ time_ms
37
+ else
38
+ -time_ms
39
+ end
40
+ end
41
+ end
42
+
43
+ #
44
+ # Adds a new entry to the repository
45
+ #
46
+ # @returns [String] entry file path
47
+ #
48
+ def create
49
+ entry_file_name = "#{current_time}_#{LOG_FILE_SUFFIX}"
50
+
51
+ entry_file_path = File.join(@config_store.dir, entry_file_name)
52
+
53
+ template = LogTemplateRenderer.new(@config_store.template_file_path)
54
+
55
+ unless File.exist?(entry_file_path)
56
+ # Add default boiler plate if the file does not exist yet
57
+ File.open(entry_file_path, "w") do |f|
58
+ f.write template.render
59
+ end
60
+ end
61
+
62
+ entry_file_path
63
+ end
64
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rsync"
4
+
5
+ # FIXME: Create module
6
+ class Repository
7
+ #
8
+ # SyncManager is an abstraction class for managing any necessity to sync
9
+ # files on the file system using Rsync.
10
+ #
11
+ class SyncManager
12
+ #
13
+ # @param config_store [Repository::ConfigStore]
14
+ #
15
+ def initialize(config_store)
16
+ @config_store = config_store
17
+ end
18
+
19
+ # Run rsync with -a to copy directories recursively
20
+
21
+ # Use trailing slash to avoid sub-directory
22
+ # See rsync manual page
23
+ #
24
+ # @throws Error if sync fails
25
+ def run
26
+ dest_path = @config_store.values.mirror.path
27
+ src_path = config_store_path_with_trailing
28
+
29
+ runner.run("-av", src_path, dest_path) do |result|
30
+ if result.success?
31
+ puts "Mirror sync complete."
32
+ result.changes.each do |change|
33
+ puts "#{change.filename} (#{change.summary})"
34
+ end
35
+ else
36
+ raise "Failed to sync: #{result.error}"
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ #
44
+ # Utility method for getting access to the runner program
45
+ # @returns [Rsync]
46
+ #
47
+ def runner
48
+ @runner ||= Rsync
49
+ end
50
+
51
+ # Depending on the runner (Rsync) program,
52
+ # you may need a trailing slash on the directory path
53
+ #
54
+ # @returns [String]
55
+ def config_store_path_with_trailing
56
+ @config_store.dir[-1] == "/" ? @config_store.dir : @config_store.dir + "/"
57
+ end
58
+ end
59
+ end
@@ -2,61 +2,46 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "tty-prompt"
5
- require "yaml"
6
- require "rsync"
7
5
  require "pry"
8
6
  require "time"
7
+
8
+ require_relative "repository/config_store"
9
9
  require_relative "editor"
10
+ require_relative "repository/sync_manager"
11
+ require_relative "helper/time_helper"
12
+ require_relative "repository/log_manager"
13
+ require_relative "repository/issue_manager"
10
14
 
11
- # Repostiroy is an accessor object for the devlogs directory
15
+ # Repository is an accessor object for the devlogs directory
12
16
  class Repository
13
- CONFIG_FILE = ".devlogs.config.yml"
14
-
15
- # TODO: should be part of configuration
16
- DEFAULT_LOG_SUFFIX = "devlogs.md"
17
- DEFAULT_DIRECTORY_PATH = "."
18
- DEFAULT_DIRECTORY_NAME = "_devlogs"
19
-
20
- # Example: 11-22-2022_1343
21
- DEFAULT_TIME_FORMAT_FILE_PREFIX = "%m-%d-%Y__%kh%Mm"
22
- DEFAULT_TIME_FORMAT_TEXT_ENTRY = "%m-%d-%Y %k:%M"
17
+ include TimeHelper
23
18
 
24
- VALID_DIRECTION = %i[asc desc].freeze
25
-
26
- # Initializes a _devlogs repository with the supplied configuration
27
- # @param repo_config [Repository::Config]
19
+ # Initializes a .devlogs repository with the supplied configuration
28
20
  #
29
- def initialize(repo_config)
30
- @config = repo_config
21
+ def initialize(repo_config_store)
22
+ @config_store = repo_config_store
23
+ @repo_config = @config_store.values
31
24
  end
32
25
 
33
- # Creates a new _devlogs entry for recording session completion
26
+ # Creates a new .devlogs entry for recording session completion
34
27
  #
35
28
  # @returns nil
36
29
  def create
37
- time = Time.new
38
- prefix = time.strftime(DEFAULT_TIME_FORMAT_FILE_PREFIX)
39
-
40
- entry_file_name = "#{prefix}_#{DEFAULT_LOG_SUFFIX}"
30
+ entry_file_path = log_manager.create_entry
41
31
 
42
- entry_file_path = File.join(@config.path, entry_file_name)
32
+ Editor.open(entry_file_path)
43
33
 
44
- unless File.exist?(entry_file_path)
45
- # Add default boiler plate if the file does not exist yet
46
- File.open(entry_file_path, "w") do |f|
47
- f.write <<~ENDOFFILE
48
- # #{time.strftime(DEFAULT_TIME_FORMAT_TEXT_ENTRY)}
49
- Tags: #dev, #log
34
+ puts "Writing entry to #{entry_file_path}.."
35
+ end
50
36
 
51
- What did you do today?
37
+ #
38
+ # @returns nil
39
+ def create_issue
40
+ issue_file_path = issue_manager.create
52
41
 
53
- ENDOFFILE
54
- end
55
- end
42
+ Editor.open(issue_file_path)
56
43
 
57
- Editor.open(entry_file_path)
58
-
59
- puts "Writing entry to #{entry_file_path}.."
44
+ puts "Writing issue to #{issue_file_path}.."
60
45
  end
61
46
 
62
47
  # Syncs the directory changes to the (optional) mirror repository
@@ -64,42 +49,17 @@ class Repository
64
49
  #
65
50
  # @returns nil
66
51
  def sync
67
- if @config.mirror?
68
- # Run rsync with -a to copy directories recursively
69
-
70
- # Use trailing slash to avoid sub-directory
71
- # See rsync manual page
72
-
73
- Rsync.run("-av", @config.path(with_trailing: true), @config.mirror.path) do |result|
74
- if result.success?
75
- puts "Mirror sync complete."
76
- result.changes.each do |change|
77
- puts "#{change.filename} (#{change.summary})"
78
- end
79
- else
80
- raise "Failed to sync: #{result.error}"
81
- end
82
- end
83
- end
52
+ sync_manager.run if @repo_config.mirror?
84
53
  end
85
54
 
86
55
  # Lists the files in the repository
87
56
  def ls(direction = :desc)
88
- raise ArgumentError, "Must be one of: " + VALID_DIRECTION unless VALID_DIRECTION.include?(direction.to_sym)
89
-
90
- Dir.glob(File.join(@config.path, "*_#{DEFAULT_LOG_SUFFIX}")).sort_by do |fpath|
91
- # The date is joined by two underscores to the suffix
92
- date, = File.basename(fpath).split("__")
93
-
94
- time_ms = Time.strptime(date, "%m-%d-%Y").to_i
57
+ log_manager.list(direction)
58
+ end
95
59
 
96
- # Descending
97
- if direction == :asc
98
- time_ms
99
- else
100
- -time_ms
101
- end
102
- end
60
+ # Lists the issues in the repository
61
+ def ls_issues(direction = :desc)
62
+ issue_manager.list(direction)
103
63
  end
104
64
 
105
65
  class << self
@@ -107,129 +67,24 @@ class Repository
107
67
  #
108
68
  # @returns [Repository]
109
69
  #
110
- def load(path = File.join(DEFAULT_DIRECTORY_PATH, DEFAULT_DIRECTORY_NAME))
111
- exists = File.exist?(path)
112
-
113
- raise "no repository found #{path}" unless exists
114
-
115
- cfg = YAML.load_file(File.join(path, CONFIG_FILE))
116
-
117
- cfg[:path] = path
118
-
119
- repo_config = Config.hydrate(cfg)
70
+ def load(path = File.join(Repository::ConfigStore::DEFAULT_DIRECTORY_PATH, Repository::ConfigStore::DEFAULT_DIRECTORY_NAME))
71
+ store = Repository::ConfigStore.load_from(path)
120
72
 
121
- new(repo_config)
73
+ new(store)
122
74
  end
123
75
  end
124
76
 
125
- # Config is a configuration data object for storing Repository configuration
126
- # in memory for access.
127
- class Config
128
- attr_reader :name, :description, :mirror, :path_value
77
+ private
129
78
 
130
- # Configuration associated with the Mirror
131
- MirrorConfig = Struct.new(:use_mirror, :path, keyword_init: true)
132
-
133
- def initialize(name, desc, mirror, p)
134
- @name = name
135
- @description = desc
136
- @mirror = MirrorConfig.new(mirror)
137
- @path_value = p
138
- end
139
-
140
- # Returns whether or not the devlogs repository is configured to mirror
141
- #
142
- # @returns [Boolean]
143
- def mirror?
144
- @mirror.use_mirror
145
- end
146
-
147
- def path(with_trailing: false)
148
- if with_trailing
149
- @path_value[-1] == "/" ? @path_value : @path_value + "/"
150
- else
151
- @path_value
152
- end
153
- end
154
-
155
- # Utility method to build a configuration from a Hash
156
- #
157
- # @returns [Repository::Config]
158
- def self.hydrate(cfg)
159
- new(cfg[:name], cfg[:description], cfg[:mirror], cfg[:path])
160
- end
79
+ def sync_manager
80
+ @sync_manager ||= Repository::SyncManager.new(@config_store)
161
81
  end
162
82
 
163
- # Initialize is an execution object which initializes a Repository on the
164
- # filesystem
165
- class Initialize
166
- # Creates a new devlogs repository at the provided path
167
- def self.run(opts = {}, path = File.join(DEFAULT_DIRECTORY_PATH, DEFAULT_DIRECTORY_NAME))
168
- exists = File.exist?(path)
169
-
170
- if exists && !opts[:force]
171
- puts "Log repository already exists in #{path}. Aborting..."
172
- raise RuntimeError
173
- end
174
-
175
- results = prompt_for_info
176
-
177
- FileUtils.mkdir_p(path)
178
- config_file = File.join(path, CONFIG_FILE)
179
-
180
- # Replace spaces in project name with underscores
181
- sanitized_project_name = results[:name].gsub(/ /, "_").downcase
182
-
183
- info_file_name = "#{sanitized_project_name}_devlogs.info.md"
184
- info_file = File.join(path, info_file_name)
185
-
186
- # Create config file
187
- File.open(config_file, "w") do |f|
188
- f.write results.to_yaml
189
- end
190
-
191
- # Create the info file
192
- File.open(info_file, "w") do |f|
193
- f.puts "# #{results[:name]}"
194
- f.puts (results[:desc]).to_s
195
- end
196
-
197
- # Git ignore if specified
198
- if results[:gitignore]
199
- gitignore = File.join(path, ".gitignore")
200
-
201
- File.open(gitignore, "a") do |f|
202
- f.puts DEFAULT_DIRECTORY_NAME
203
- end
204
- end
205
- end
83
+ def log_manager
84
+ @log_manager ||= LogManager.new(@config_store)
85
+ end
206
86
 
207
- # Creates an interactive prompt for user input
208
- #
209
- # @returns [Hash]
210
- def self.prompt_for_info
211
- prompt = TTY::Prompt.new
212
-
213
- prompt.collect do |_p|
214
- # Project name
215
- key(:name).ask("What is the project name?") do |q|
216
- q.required true
217
- end
218
-
219
- # Project description
220
- key(:desc).ask("What is the project description?") do |q|
221
- q.required true
222
- end
223
-
224
- key(:mirror) do
225
- key(:use_mirror).ask("Do you want to mirror these logs?", convert: :boolean)
226
- key(:path).ask("Path to mirror directory: ")
227
- end
228
-
229
- key(:gitignore).ask("Do you want to gitignore the devlogs repository?") do |q|
230
- q.required true
231
- end
232
- end
233
- end
87
+ def issue_manager
88
+ @issue_manager ||= IssueManager.new(@config_store)
234
89
  end
235
90
  end
@@ -0,0 +1,13 @@
1
+ [[@INFO]]
2
+ Tags:
3
+ Links:
4
+
5
+ ---
6
+
7
+ # <%= title %>
8
+
9
+ ## Problem
10
+ <%= description %>
11
+
12
+ ## Reproduction Steps
13
+ <%= reproduction %>
@@ -0,0 +1,5 @@
1
+ # LOG: <%= @time %>
2
+ Tags: #dev, #log
3
+
4
+ What did you do today?
5
+
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Devlogs
4
- VERSION = "0.3.1"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/devlogs.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "devlogs/gem"
3
4
  require_relative "devlogs/cli"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devlogs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - aquaflamingo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-14 00:00:00.000000000 Z
11
+ date: 2022-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rsync
@@ -98,9 +98,23 @@ files:
98
98
  - lib/devlogs/cli.rb
99
99
  - lib/devlogs/editor.rb
100
100
  - lib/devlogs/executable.rb
101
+ - lib/devlogs/gem.rb
102
+ - lib/devlogs/helper/time_helper.rb
103
+ - lib/devlogs/helper/tty_prompt_helper.rb
101
104
  - lib/devlogs/pager.rb
102
- - lib/devlogs/prompt_utils.rb
105
+ - lib/devlogs/render/erb_template_renderer.rb
106
+ - lib/devlogs/render/issue_template_renderer.rb
107
+ - lib/devlogs/render/log_template_renderer.rb
103
108
  - lib/devlogs/repository.rb
109
+ - lib/devlogs/repository/config.rb
110
+ - lib/devlogs/repository/config_builder.rb
111
+ - lib/devlogs/repository/config_store.rb
112
+ - lib/devlogs/repository/initializer.rb
113
+ - lib/devlogs/repository/issue_manager.rb
114
+ - lib/devlogs/repository/log_manager.rb
115
+ - lib/devlogs/repository/sync_manager.rb
116
+ - lib/devlogs/templates/__issue_template.erb.md
117
+ - lib/devlogs/templates/__log_template.erb.md
104
118
  - lib/devlogs/version.rb
105
119
  homepage: http://github.com/aquaflamingo/devlogs
106
120
  licenses: