devlogs 0.3.1 → 1.1.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
  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: