From a13250dc96dd9f4c73aa85b3682663e6dfce747a Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sun, 3 Sep 2023 12:25:02 -0400 Subject: [PATCH 01/16] return sid of task when adding task --- README.md | 2 +- backlog/.imdone/config.yml | 5 +++++ backlog/README.md | 11 +++++++++++ cli.js | 4 ++-- devops/imdone/config.yml | 5 +++++ lib/{controlers => cli}/CliControler.js | 8 +++++--- lib/file.js | 16 +++++++++++++++- lib/project.js | 4 ++-- lib/repository.js | 2 +- notes/story-tasks.md | 2 +- 10 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 backlog/README.md rename lib/{controlers => cli}/CliControler.js (92%) diff --git a/README.md b/README.md index 29b417d3..1eeaa650 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Downloads](https://img.shields.io/npm/dm/imdone-core.svg)](https://npmjs.org/package/imdone-core) [![Build Status](https://github.com/imdone/imdone-core/actions/workflows/ci.yml/badge.svg) -Imdone is text based kanban processor with a simple syntax that uses [comment tags like TODO and FIXME](https://en.wikipedia.org/wiki/Comment_%28computer_programming%29#Tags) and [todo.txt format](https://github.com/todotxt/todo.txt#todotxt-format). This model allows the user to create and modify tasks using the keyboard and automatically establishes a link between their tasks and work. Get [imdone](https://imdone.io) to see your projects board and this library in action. +Imdone is text based kanban processor with a simple syntax that uses [comment tags like TODO and FIXME](https://en.wikipedia.org/wiki/Comment_%28computer_programming%29#Tags) and [todo.txt format](https://github.com/todotxt/todo.txt#todotxt-format). This model allows the user to create and modify tasks using the keyboard and automatically establishes a link between their tasks and work. Get [imdone](https://imdone.io) or use the cli to see your projects board and this library in action. ![imdone-screenshot.png (5120×2838)](https://imdone.io/docs/images/card-anatomy.png) diff --git a/backlog/.imdone/config.yml b/backlog/.imdone/config.yml index cf028d06..0187f066 100644 --- a/backlog/.imdone/config.yml +++ b/backlog/.imdone/config.yml @@ -13,7 +13,12 @@ code: - IDEA - NOTE - REVIEW + - BACKLOG lists: + - name: BACKLOG + hidden: false + ignore: false + id: jea67colm3nm22s - hidden: false name: TODO id: jea6phpllxpsj7j diff --git a/backlog/README.md b/backlog/README.md new file mode 100644 index 00000000..1e176455 --- /dev/null +++ b/backlog/README.md @@ -0,0 +1,11 @@ +imdone backlog +==== + +This backlog contains folders for each backlog item. To add a backlog item, open the folder in imdone or use the cli and create a task with an is-epic tag. + +## Using the cli +Run this from the root of the project to add a story +```bash +npx imdone add -p backlog -t is-epic -l BACKLOG "Import story tasks from markdown" +``` + diff --git a/cli.js b/cli.js index c629ea02..34f3699a 100755 --- a/cli.js +++ b/cli.js @@ -1,7 +1,7 @@ #!/usr/bin/env node const { program } = require('commander'); -const { imdoneInit, addTask, listTasks } = require('./lib/controlers/CliControler') +const { imdoneInit, addTask, listTasks } = require('./lib/cli/CliControler') const package = require('./package.json') const { log, info, warn, logQueue } = hideLogs() @@ -27,7 +27,7 @@ program .option('-c, --contexts ', 'The contexts to use') .action(async function () { let { projectPath = process.env.PWD, list, tags, contexts } = this.opts() - await addTask(this.args[0], projectPath, list, tags, contexts) + await addTask({task: this.args[0], projectPath, list, tags, contexts, log}) }) program diff --git a/devops/imdone/config.yml b/devops/imdone/config.yml index cf028d06..0187f066 100644 --- a/devops/imdone/config.yml +++ b/devops/imdone/config.yml @@ -13,7 +13,12 @@ code: - IDEA - NOTE - REVIEW + - BACKLOG lists: + - name: BACKLOG + hidden: false + ignore: false + id: jea67colm3nm22s - hidden: false name: TODO id: jea6phpllxpsj7j diff --git a/lib/controlers/CliControler.js b/lib/cli/CliControler.js similarity index 92% rename from lib/controlers/CliControler.js rename to lib/cli/CliControler.js index 9fa7866d..905496d3 100644 --- a/lib/controlers/CliControler.js +++ b/lib/cli/CliControler.js @@ -9,7 +9,6 @@ newConfigFromFile = async (configPath) => { return new Config(loadYAML(config)) } - module.exports = { imdoneInit: async function (projectPath, configPath) { projectPath = resolve(projectPath) @@ -22,11 +21,14 @@ module.exports = { return await project.init() }, - addTask: async function (task, projectPath, list, tags, contexts) { + addTask: async function ({task, projectPath, list, tags, contexts, log}) { projectPath = resolve(projectPath) const project = createFileSystemProject({path: projectPath}) await project.init() - const data = await project.addTaskToFile({list, content: task, tags, contexts}) + const file = await project.addTaskToFile({list, content: task, tags, contexts}) + file.rollback() + .extractTasks(project.config) + log(file.tasks[0].meta.sid[0]) }, listTasks: async function (projectPath, filter, json, log) { diff --git a/lib/file.js b/lib/file.js index 862c0878..d5a07331 100644 --- a/lib/file.js +++ b/lib/file.js @@ -696,7 +696,6 @@ File.prototype.modifyTask = function (task, config, noEmit) { if (this.getContent().trim() === newContent.trim()) return this.setContent(newContent) this.setModified(true) - console.log('modifyTask setModified true for file:', this.path) if (!noEmit) { this.emit('task.modified', task) this.emit('file.modified', this) @@ -979,10 +978,25 @@ File.prototype.getPath = function () { return this.path } +File.prototype.getFullPath = function () { + return this.repoId + this.path +} + File.prototype.getId = function () { return this.getPath() } +File.prototype.reset = function () { + this.previousContent = this.content + this.content = null + return this +} + +File.prototype.rollback = function () { + this.content = this.previousContent + return this +} + File.prototype.setContent = function (content) { this.content = content return this diff --git a/lib/project.js b/lib/project.js index 30e1aa85..9b1ae380 100644 --- a/lib/project.js +++ b/lib/project.js @@ -385,9 +385,9 @@ module.exports = class WorkerProject extends Project { } const cardContent = content + cardData.template return new Promise((resolve, reject) => { - this.repo.addTaskToFile(filePath, list, cardContent, (err) => { + this.repo.addTaskToFile(filePath, list, cardContent, (err, file) => { if (err) return reject(err) - resolve(cardData) + resolve(file) }) }) } diff --git a/lib/repository.js b/lib/repository.js index 9f25fd5a..ac2ac30c 100644 --- a/lib/repository.js +++ b/lib/repository.js @@ -518,7 +518,7 @@ Repository.prototype.getFilesWithTasks = function () { } Repository.prototype.resetFile = function (file) { - file.content = null + file.reset() file.removeListener('task.found', this.taskFoundListener) file.removeListener('task.modified', this.taskModifiedListener) } diff --git a/notes/story-tasks.md b/notes/story-tasks.md index 93babbed..c2e047a1 100644 --- a/notes/story-tasks.md +++ b/notes/story-tasks.md @@ -4,7 +4,7 @@ is-epic:"story tasks" - [ ] #READY As a developer I would like to quickly add story tasks in my repo so they can be published for stakeholder visibility. - + **Scenarios** - [x] Developer relizes a new IaC task must be added to the story, so they run `imdone add "Add IaC for new GET orders endpoint"` - [x] Developer realizesa new IaC task must be added to the story, so they open imdone and add a new task with the text `Add IaC for new GET orders endpoint` From 7e127944a2cda2f12968a214a2326da6a3260b52 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sun, 3 Sep 2023 15:15:20 -0400 Subject: [PATCH 02/16] Add MVP tasks for story tasks cli --- backlog/.imdone/properties/card.js | 2 +- backlog/README.md | 88 +++++++++++++++++++++++++++++- devops/imdone/properties/card.js | 2 +- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/backlog/.imdone/properties/card.js b/backlog/.imdone/properties/card.js index e587d64d..536ed2fc 100644 --- a/backlog/.imdone/properties/card.js +++ b/backlog/.imdone/properties/card.js @@ -12,6 +12,6 @@ const generateRandomString = (length) => { module.exports = function (task) { const project = this.project return { - sid: generateRandomString(8) + sid: generateRandomString(5) } } \ No newline at end of file diff --git a/backlog/README.md b/backlog/README.md index 1e176455..bc7328c5 100644 --- a/backlog/README.md +++ b/backlog/README.md @@ -1,11 +1,93 @@ imdone backlog ==== -This backlog contains folders for each backlog item. To add a backlog item, open the folder in imdone or use the cli and create a task with an is-epic tag. +This directory contains folders for each backlog item. -## Using the cli +## Work on a story +### After collaborative design, import a story and story tasks from markdown +```bash +npx imdone import story < +``` +- [ ] `./backlog` is the default project folder +- [ ] Initialize imdone in the backlog folder +- [ ] `` should be the markdown title +- [ ] On import always remove the contents of the `backlog/story/` + +### Day to day work after collaborative story design + +#### List tasks in a story +```bash +npx imdone ls -p backlog -s +``` +- [ ] `./backlog` is the default project folder +- [ ] If the current branch starts with `story/`, parse the story id from the branch name +- [ ] Can also pass in the story id with the `-s option +- [ ] Can also use the filter option + +#### List tasks in a story and group +```bash +npx imdone ls -p backlog -s -g +``` +- [ ] `./backlog` is the default project folder +- [ ] If the current branch starts with `story/`, parse the story id and group from the branch name +- [ ] Can also use the filter option + +#### Update task +```bash +npx imdone update task -p backlog -g +``` +- [ ] This should move a task to a different group and/or change it's text + +#### Start a task +```bash +npx imdone start -p backlog +``` +- [ ] This should find the task and create a branch off of main named `story////` +- [ ] If the branch exists, check it out + +#### Add breadcrumbs for the next developer or ensemble +1. Open the file with the same name as the branch under backlog and add content!!! +2. commit and push the branch or run `mob done` + +#### Complete a task +```bash +npx imdone done +``` + +## Adding tasks without import + +### Initializing a backlog +```bash +npx imdone init -p backlog +``` +- [ ] `./backlog` is the default project folder +- [ ] Make `devops/imdone` the defaults for init + +### Add a story Run this from the root of the project to add a story ```bash -npx imdone add -p backlog -t is-epic -l BACKLOG "Import story tasks from markdown" +npx imdone add story -p backlog -s -l BACKLOG "Add a story from the command line" +``` +- [ ] `./backlog` is the default project folder +- [ ] This should initialize a new imdone project in `backlog/` and return the meta sid +- [ ] The task should be in `backlog/story//README.md` +- [ ] The task should have `task-id:` meta and `story` tag + +### Add a story task +```bash +npx imdone add task -p backlog -s -l TODO "Add a story task from the command line" ``` +- [ ] `./backlog` is the default project folder +- [ ] This should initialize a new imdone project in `backlog/story//ungrouped`, containing a task with `task-id:` and `story-id: +- [ ] use default list if no list is present +### List task groups for a story +Use ls to list the directories in `backlog/story/` + +### Add a story task to a group +```bash +npx imdone add task -p backlog -s -g "" -l TODO "Add a story task with group from the command line" +``` +- [ ] `./backlog` is the default project folder +- [ ] This should initialize a new imdone project in `backlog/story//`, containing a task with `story-id:`, `group:` and `task-id:` meta and return the +- [ ] use default list if no list is present diff --git a/devops/imdone/properties/card.js b/devops/imdone/properties/card.js index e587d64d..536ed2fc 100644 --- a/devops/imdone/properties/card.js +++ b/devops/imdone/properties/card.js @@ -12,6 +12,6 @@ const generateRandomString = (length) => { module.exports = function (task) { const project = this.project return { - sid: generateRandomString(8) + sid: generateRandomString(5) } } \ No newline at end of file From ad5a6bb15b3232b0e48febd192dd0dfb09f1ef51 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Tue, 5 Sep 2023 23:37:26 -0400 Subject: [PATCH 03/16] Start on import with parser --- backlog/README.md | 55 ++++++++--- cli.js | 37 ++++++- lib/cli/CliControler.js | 98 +++++++++++-------- lib/cli/__tests__/markdownStoryParser.spec.js | 36 +++++++ lib/cli/markdownStoryParser.js | 78 +++++++++++++++ lib/tools.js | 3 + 6 files changed, 245 insertions(+), 62 deletions(-) create mode 100644 lib/cli/__tests__/markdownStoryParser.spec.js create mode 100644 lib/cli/markdownStoryParser.js diff --git a/backlog/README.md b/backlog/README.md index bc7328c5..00187c1d 100644 --- a/backlog/README.md +++ b/backlog/README.md @@ -12,9 +12,41 @@ npx imdone import story < - [ ] Initialize imdone in the backlog folder - [ ] `` should be the markdown title - [ ] On import always remove the contents of the `backlog/story/` +- [ ] Shold handle a file with the following format +```markdown +# + +This is the story summary + +## Tasks +- [ ] An ungrouped task + +### +- [ ] A task in a group + +### +- [ ] A task in a group +``` +- [ ] use `markdown-it.parse` to create AST ### Day to day work after collaborative story design +#### Start a task +```bash +npx imdone start -p backlog +``` +- [ ] This should find the task and create a branch off of main named `story////` +- [ ] If the branch exists, check it out + +#### Add breadcrumbs for the next developer or ensemble +1. Open the file with the same name as the branch under backlog and add content!!! +2. commit and push the branch or run `mob done` + +#### Complete a task +```bash +npx imdone done +``` + #### List tasks in a story ```bash npx imdone ls -p backlog -s @@ -33,27 +65,13 @@ npx imdone ls -p backlog -s -g - [ ] Can also use the filter option #### Update task + +##### With the CLI ```bash npx imdone update task -p backlog -g ``` - [ ] This should move a task to a different group and/or change it's text -#### Start a task -```bash -npx imdone start -p backlog -``` -- [ ] This should find the task and create a branch off of main named `story////` -- [ ] If the branch exists, check it out - -#### Add breadcrumbs for the next developer or ensemble -1. Open the file with the same name as the branch under backlog and add content!!! -2. commit and push the branch or run `mob done` - -#### Complete a task -```bash -npx imdone done -``` - ## Adding tasks without import ### Initializing a backlog @@ -91,3 +109,8 @@ npx imdone add task -p backlog -s -g "" -l TODO "Add a s - [ ] `./backlog` is the default project folder - [ ] This should initialize a new imdone project in `backlog/story//`, containing a task with `story-id:`, `group:` and `task-id:` meta and return the - [ ] use default list if no list is present + +#### With the UI +- Open the story folder as a project in imdone +- Under board actions select the **Create task in ** from the menu + diff --git a/cli.js b/cli.js index 34f3699a..352259df 100755 --- a/cli.js +++ b/cli.js @@ -1,11 +1,12 @@ #!/usr/bin/env node const { program } = require('commander'); -const { imdoneInit, addTask, listTasks } = require('./lib/cli/CliControler') +const { imdoneInit, importMarkdown, addTask, listTasks } = require('./lib/cli/CliControler') const package = require('./package.json') +const path = require('path') const { log, info, warn, logQueue } = hideLogs() - +const defaultProjectPath = path.join(process.env.PWD, 'backlog') // TODO ## Add an option to add properties/card.js program .version(package.version, '-v, --version', 'output the current version') @@ -14,10 +15,36 @@ program .option('-p, --project-path ', 'The path to the imdone project') .option('-c, --config-path ', 'The path to the imdone config file') .action(async function () { - let { projectPath = process.env.PWD, configPath } = this.opts() + let { projectPath = defaultProjectPath, configPath } = this.opts() await imdoneInit(projectPath, configPath) }) +program +.command('import') +.description('import markdown from STDIN') +.option('-p, --project-path ', 'The path to the imdone project') +.option('-c, --config-path ', 'The path to the imdone config file') +.action(async function () { + let { projectPath = defaultProjectPath, configPath } = this.opts() + log(process.stdin.isTTY) + const isTTY = process.stdin.isTTY; + const stdin = process.stdin; + if (isTTY) return console.error('Markdown must be provided as stdin') + + var data = ''; + + stdin.on('readable', function() { + var chuck = stdin.read(); + if(chuck !== null){ + data += chuck; + } + }); + stdin.on('end', async function() { + await importMarkdown(projectPath, configPath, data, log) + }); +}) + + program .command('add ') .description('add a task') @@ -26,7 +53,7 @@ program .option('-t, --tags ', 'The tags to use') .option('-c, --contexts ', 'The contexts to use') .action(async function () { - let { projectPath = process.env.PWD, list, tags, contexts } = this.opts() + let { projectPath = defaultProjectPath, list, tags, contexts } = this.opts() await addTask({task: this.args[0], projectPath, list, tags, contexts, log}) }) @@ -37,7 +64,7 @@ program .option('-f, --filter ', 'The filter to use') .option('-j, --json', 'Output as json') .action(async function () { - let { projectPath = process.env.PWD, filter, json } = this.opts() + let { projectPath = defaultProjectPath, filter, json } = this.opts() await listTasks(projectPath, filter, json, log) }) program.parse(); diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index 905496d3..862264e1 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -3,52 +3,68 @@ const { createFileSystemProject } = require('../project-factory') const { loadYAML } = require('../tools') const { readFile } = require('fs/promises') const Config = require('../config') +const markdownStoryParser = require('./markdownStoryParser') +const { mkdir } = require('fs/promises') +const path = require('path') -newConfigFromFile = async (configPath) => { - const config = await readFile(configPath, 'utf8') - return new Config(loadYAML(config)) +const STORIES_DIR = 'stories' +const CliController = {} + +CliController.imdoneInit = async function (projectPath, configPath) { + projectPath = resolve(projectPath) + let config + if (configPath) { + configPath = resolve(configPath) + config = await newConfigFromFile(configPath) + } + await mkdir(path.join(projectPath, STORIES_DIR), {recursive: true}) + const project = createFileSystemProject({path: projectPath, config}) + return await project.init() } -module.exports = { - imdoneInit: async function (projectPath, configPath) { - projectPath = resolve(projectPath) - let config - if (configPath) { - configPath = resolve(configPath) - config = await newConfigFromFile(configPath) - } - const project = createFileSystemProject({path: projectPath, config}) - return await project.init() - }, +CliController.addTask = async function ({task, projectPath, list, tags, contexts, log}) { + projectPath = resolve(projectPath) + const project = createFileSystemProject({path: projectPath}) + await project.init() + const file = await project.addTaskToFile({list, content: task, tags, contexts}) + file.rollback() + .extractTasks(project.config) + log(file.tasks[0].meta.sid[0]) +} - addTask: async function ({task, projectPath, list, tags, contexts, log}) { - projectPath = resolve(projectPath) - const project = createFileSystemProject({path: projectPath}) - await project.init() - const file = await project.addTaskToFile({list, content: task, tags, contexts}) - file.rollback() - .extractTasks(project.config) - log(file.tasks[0].meta.sid[0]) - }, +CliController.listTasks = async function (projectPath, filter, json, log) { + projectPath = resolve(projectPath) + const project = createFileSystemProject({path: projectPath}) + await project.init() + const tasks = project.getCards(filter) + const lists = project.getLists({tasks}) - listTasks: async function (projectPath, filter, json, log) { - projectPath = resolve(projectPath) - const project = createFileSystemProject({path: projectPath}) - await project.init() - const tasks = project.getCards(filter) - const lists = project.getLists({tasks}) - - if (json) return log(JSON.stringify(lists, null, 2)) - - if (project.getCards('meta.group=*', tasks).length > 0) { - const groupedTasks = getGroupedTasks(tasks) - logProject(log, project) - logGroupedTasks(log, project, groupedTasks) - } else { - logProject(log, project) - logLists(log, project, lists, '##') - } - } + if (json) return log(JSON.stringify(lists, null, 2)) + + if (project.getCards('meta.group=*', tasks).length > 0) { + const groupedTasks = getGroupedTasks(tasks) + logProject(log, project) + logGroupedTasks(log, project, groupedTasks) + } else { + logProject(log, project) + logLists(log, project, lists, '##') + } +} + +CliController.importMarkdown = async function(projectPath, configPath, markdown, log) { + await CliController.imdoneInit(projectPath, configPath) + const { storyId, description, tasks } = markdownStoryParser(markdown) + // TODO delete ${projectPath}/story/storyId + const project = createFileSystemProject({path: projectPath}) + await project.init() +} + +module.exports = CliController + +// Helper Functions +newConfigFromFile = async (configPath) => { + const config = await readFile(configPath, 'utf8') + return new Config(loadYAML(config)) } function getGroupedTasks(tasks) { diff --git a/lib/cli/__tests__/markdownStoryParser.spec.js b/lib/cli/__tests__/markdownStoryParser.spec.js new file mode 100644 index 00000000..d1621723 --- /dev/null +++ b/lib/cli/__tests__/markdownStoryParser.spec.js @@ -0,0 +1,36 @@ +const parse = require("../markdownStoryParser") +const expect = require("chai").expect +describe('markdownStoryParser', () => { + it('should parse a markdown story name', () => { + const markdown = `# story-id + +This is the story description. + +## Tasks +- [ ] An unfinished task + +### Phase one (Interfaces) +- [ ] A task in phase one +- [ ] Another task in phase one + - [ ] A sub task in phase one + Some more data about the task + +### Phase two (Implementation) +- [ ] A task in phase two +` + const { name, description, tasks } = parse(markdown) + expect(name).to.equal('story-id') + expect(description).to.equal('This is the story description.') + expect(tasks.length).to.equal(5) + expect(tasks[0].text).to.equal('An unfinished task') + expect(tasks[0].group).to.equal('ungrouped') + expect(tasks[1].text).to.equal('A task in phase one') + expect(tasks[1].group).to.equal('Phase one (Interfaces)') + expect(tasks[2].text).to.equal('Another task in phase one') + expect(tasks[2].group).to.equal('Phase one (Interfaces)') + expect(tasks[3].text).to.equal("A sub task in phase one\nSome more data about the task") + expect(tasks[3].group).to.equal('Phase one (Interfaces)') + expect(tasks[4].text).to.equal('A task in phase two') + expect(tasks[4].group).to.equal('Phase two (Implementation)') + }) +}) \ No newline at end of file diff --git a/lib/cli/markdownStoryParser.js b/lib/cli/markdownStoryParser.js new file mode 100644 index 00000000..4d2daf3b --- /dev/null +++ b/lib/cli/markdownStoryParser.js @@ -0,0 +1,78 @@ +const { parseMD } = require('../tools') + +function isOpen(token, type, tag = token.tag) { + return token.type === type + '_open' && token.tag === tag +} + +function isClose(token, type, tag = token.tag) { + return token.type === type + '_close' && token.tag === tag +} + +function getHeadingLevel(tag) { + return parseInt(tag.substring(1), 10) +} + +module.exports = function parse(markdown) { + const heading = 'heading' + const paragraph = 'paragraph' + const inline = 'inline' + const bulletList = 'bullet_list' + const listItem = 'list_item' + let storyId, description, groupName = '', tasks = [] + let inHeading = false + let inParagraph = false + let inBulletList = false + let inListItem = false + let inTasks = false + let headingLevel = 0 + const ast = parseMD(markdown) + + ast.forEach((token) => { + const { type, tag, markup, content } = token + if (isOpen(token, heading)) { + inHeading = true + headingLevel = getHeadingLevel(tag) + } else if (isClose(token, heading)) { + inHeading = false + } + + if (isOpen(token, bulletList)) { + inBulletList = true + } else if (isClose(token, bulletList)) { + inBulletList = false + } + + if (isOpen(token, listItem)) { + inListItem = true + } else if (isClose(token, listItem)) { + inListItem = false + } + + if (isOpen(token, paragraph)) { + inParagraph = true + } else if (isClose(token, paragraph)) { + inParagraph = false + } + + if (inHeading && type == inline) { + groupName = content + if (headingLevel == 1) { + storyId = groupName + } + } + + if (inParagraph && headingLevel == 1 && type == inline) { + description = content + } + + if (groupName.toLowerCase().endsWith('tasks') && headingLevel == 2) { + inTasks = true + } + + if (inTasks && inBulletList && inListItem && inParagraph && type == inline) { + const group = headingLevel > 2 ? groupName : 'ungrouped' + tasks.push({ group, text: content.replace(/^\[.\]\s/, '') }) + } + }) + return { name: storyId, description, tasks } +} \ No newline at end of file diff --git a/lib/tools.js b/lib/tools.js index bb4aa43b..df3297ec 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -94,6 +94,9 @@ module.exports = { marked: function (content) { return markdown.render(content) }, + parseMD: function (content) { + return markdown.parse(content, {}) + }, /** * Description From 2739ba5e1c53330747bb37996dd7d688a2e6a2e8 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Wed, 6 Sep 2023 23:51:09 -0400 Subject: [PATCH 04/16] Complete Task import! --- backlog/.imdone/config.yml | 63 ----------------------- backlog/.imdone/properties/card.js | 17 ------- backlog/README.md | 16 +++--- cli.js | 16 +++--- devops/imdone/actions/board.js | 13 +++++ devops/imdone/actions/card.js | 4 ++ devops/imdone/config.yml | 5 ++ lib/cli/CliControler.js | 81 +++++++++++++++++++++++------- lib/cli/markdownStoryParser.js | 2 +- lib/config.js | 12 +++++ lib/project.js | 9 ++-- 11 files changed, 119 insertions(+), 119 deletions(-) delete mode 100644 backlog/.imdone/config.yml delete mode 100644 backlog/.imdone/properties/card.js create mode 100644 devops/imdone/actions/board.js create mode 100644 devops/imdone/actions/card.js diff --git a/backlog/.imdone/config.yml b/backlog/.imdone/config.yml deleted file mode 100644 index 0187f066..00000000 --- a/backlog/.imdone/config.yml +++ /dev/null @@ -1,63 +0,0 @@ -keepEmptyPriority: true -code: - include_lists: - - TODO - - DOING - - DONE - - PLANNING - - FIXME - - ARCHIVE - - HACK - - CHANGED - - XXX - - IDEA - - NOTE - - REVIEW - - BACKLOG -lists: - - name: BACKLOG - hidden: false - ignore: false - id: jea67colm3nm22s - - hidden: false - name: TODO - id: jea6phpllxpsj7j - - hidden: false - name: DOING - id: jea6phpllxpsj7k - - hidden: false - ignore: true - name: DONE - id: jea6phpllxpsj7l -settings: - openIn: default - customOpenIn: '' - editorTheme: blackboard - journalType: New File - journalPath: . - appendNewCardsTo: imdone-tasks.md - newCardSyntax: HASHTAG - replaceSpacesWith: '-' - plugins: - devMode: false - journalTemplate: null - theme: dark - views: [] - name: backlog - cards: - colors: [] - template: | - - - trackChanges: false - metaNewLine: false - addCompletedMeta: false - addCheckBoxTasks: false - doneList: DONE - taskPrefix: '##' - tagPrefix: + - metaSep: ':' - orderMeta: true - maxLines: 6 - addNewCardsToTop: true - defaultList: TODO diff --git a/backlog/.imdone/properties/card.js b/backlog/.imdone/properties/card.js deleted file mode 100644 index 536ed2fc..00000000 --- a/backlog/.imdone/properties/card.js +++ /dev/null @@ -1,17 +0,0 @@ -const generateRandomString = (length) => { - let result = ''; - const characters = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; -}; - -module.exports = function (task) { - const project = this.project - return { - sid: generateRandomString(5) - } -} \ No newline at end of file diff --git a/backlog/README.md b/backlog/README.md index 00187c1d..6d5f3367 100644 --- a/backlog/README.md +++ b/backlog/README.md @@ -8,10 +8,10 @@ This directory contains folders for each backlog item. ```bash npx imdone import story < ``` -- [ ] `./backlog` is the default project folder -- [ ] Initialize imdone in the backlog folder -- [ ] `` should be the markdown title -- [ ] On import always remove the contents of the `backlog/story/` +- [x] `./backlog` is the default project folder +- [x] Initialize imdone in the backlog folder +- [x] `` should be the markdown title +- [x] On import always remove the contents of the `backlog/story/` - [ ] Shold handle a file with the following format ```markdown # @@ -27,7 +27,8 @@ This is the story summary ### - [ ] A task in a group ``` -- [ ] use `markdown-it.parse` to create AST +- [x] use `markdown-it.parse` to create AST +- [ ] Save story-id project path so it's available for starting a task, after a task is started, save the task-id ### Day to day work after collaborative story design @@ -87,7 +88,8 @@ Run this from the root of the project to add a story npx imdone add story -p backlog -s -l BACKLOG "Add a story from the command line" ``` - [ ] `./backlog` is the default project folder -- [ ] This should initialize a new imdone project in `backlog/` and return the meta sid +- [ ] This should initialize a new imdone project in `backlog/` +- [ ] Create a task for the story and return the meta sid - [ ] The task should be in `backlog/story//README.md` - [ ] The task should have `task-id:` meta and `story` tag @@ -96,7 +98,7 @@ npx imdone add story -p backlog -s -l BACKLOG "Add a story from the c npx imdone add task -p backlog -s -l TODO "Add a story task from the command line" ``` - [ ] `./backlog` is the default project folder -- [ ] This should initialize a new imdone project in `backlog/story//ungrouped`, containing a task with `task-id:` and `story-id: +- [ ] This should initialize a new imdone project in `backlog/story//ungrouped`, containing a task with `task-id:` and `story-id:` meta and return the - [ ] use default list if no list is present ### List task groups for a story diff --git a/cli.js b/cli.js index 352259df..ce74ae11 100755 --- a/cli.js +++ b/cli.js @@ -16,31 +16,29 @@ program .option('-c, --config-path ', 'The path to the imdone config file') .action(async function () { let { projectPath = defaultProjectPath, configPath } = this.opts() - await imdoneInit(projectPath, configPath) + await imdoneInit({projectPath, configPath}) }) program .command('import') .description('import markdown from STDIN') .option('-p, --project-path ', 'The path to the imdone project') -.option('-c, --config-path ', 'The path to the imdone config file') .action(async function () { - let { projectPath = defaultProjectPath, configPath } = this.opts() - log(process.stdin.isTTY) + let { projectPath = defaultProjectPath } = this.opts() const isTTY = process.stdin.isTTY; const stdin = process.stdin; if (isTTY) return console.error('Markdown must be provided as stdin') - var data = ''; + var markdown = ''; stdin.on('readable', function() { - var chuck = stdin.read(); - if(chuck !== null){ - data += chuck; + var chunk = stdin.read(); + if(chunk !== null){ + markdown += chunk; } }); stdin.on('end', async function() { - await importMarkdown(projectPath, configPath, data, log) + await importMarkdown(projectPath, markdown, log) }); }) diff --git a/devops/imdone/actions/board.js b/devops/imdone/actions/board.js new file mode 100644 index 00000000..a0b58f86 --- /dev/null +++ b/devops/imdone/actions/board.js @@ -0,0 +1,13 @@ +module.exports = function (task) { + const project = this.project + const groups = new Set(project.getCards().map((card) => card.meta.group && card.meta.group[0] || 'ungrouped')) + return [...groups].map((group) => { + const filterValue = encodeURIComponent(`allMeta.group = "${group}" or allTags = story`) + return { + title: group, + action: function () { + project.openUrl(`imdone://active.repo?filter=${filterValue}`) + } + } + }) +} \ No newline at end of file diff --git a/devops/imdone/actions/card.js b/devops/imdone/actions/card.js new file mode 100644 index 00000000..ceda8c67 --- /dev/null +++ b/devops/imdone/actions/card.js @@ -0,0 +1,4 @@ +module.exports = function (task) { + const project = this.project + return [] +} \ No newline at end of file diff --git a/devops/imdone/config.yml b/devops/imdone/config.yml index 0187f066..703e174a 100644 --- a/devops/imdone/config.yml +++ b/devops/imdone/config.yml @@ -1,6 +1,7 @@ keepEmptyPriority: true code: include_lists: + - NOTE - TODO - DOING - DONE @@ -15,6 +16,10 @@ code: - REVIEW - BACKLOG lists: + - hidden: false + ignore: false + name: NOTE + id: jea6phpllxpsj7m - name: BACKLOG hidden: false ignore: false diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index 862264e1..0fefe148 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -3,23 +3,33 @@ const { createFileSystemProject } = require('../project-factory') const { loadYAML } = require('../tools') const { readFile } = require('fs/promises') const Config = require('../config') -const markdownStoryParser = require('./markdownStoryParser') -const { mkdir } = require('fs/promises') +const parseMarkdownStory = require('./markdownStoryParser') +const { mkdir, rm, cp } = require('fs/promises') const path = require('path') +const eol = require('eol') const STORIES_DIR = 'stories' +const TASKS_DIR = 'tasks' +const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'config.yml') +const CARD_PROPERTIES_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'properties') +const ACTIONS_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'actions') const CliController = {} -CliController.imdoneInit = async function (projectPath, configPath) { +CliController.imdoneInit = async function ({projectPath, configPath = DEFAULT_CONFIG_PATH, tasksDir = STORIES_DIR}) { projectPath = resolve(projectPath) - let config - if (configPath) { - configPath = resolve(configPath) - config = await newConfigFromFile(configPath) - } - await mkdir(path.join(projectPath, STORIES_DIR), {recursive: true}) + configPath = resolve(configPath) + + const config = await newConfigFromFile(configPath) + config.name = path.basename(projectPath) + config.journalPath = tasksDir + + await cp(CARD_PROPERTIES_PATH, path.join(projectPath, '.imdone', 'properties'), { recursive: true }) + await cp(ACTIONS_PATH, path.join(projectPath, '.imdone', 'actions'), { recursive: true }) + await mkdir(path.join(projectPath, tasksDir), {recursive: true}) + const project = createFileSystemProject({path: projectPath, config}) - return await project.init() + await project.init() + return project } CliController.addTask = async function ({task, projectPath, list, tags, contexts, log}) { @@ -51,18 +61,53 @@ CliController.listTasks = async function (projectPath, filter, json, log) { } } -CliController.importMarkdown = async function(projectPath, configPath, markdown, log) { - await CliController.imdoneInit(projectPath, configPath) - const { storyId, description, tasks } = markdownStoryParser(markdown) - // TODO delete ${projectPath}/story/storyId - const project = createFileSystemProject({path: projectPath}) - await project.init() +CliController.importMarkdown = async function(projectPath, markdown, log) { + const project = await CliController.imdoneInit({projectPath}) + const { storyId, description, tasks } = parseMarkdownStory(markdown) + const storyProject = await createStoryProject(projectPath, storyId, log) + await addStoryTask(storyProject, storyId, eol.split(description)[0], tasks, log) + + tasks.forEach(async (task, i) => { + const order = (i + 1) * (10) + const file = await storyProject.addTaskToFile({list: 'TODO', tags: ['task'], content: task.text}) + file.rollback() + .extractTasks(project.config) + + await storyProject.addMetadata(file.tasks[0], 'group', task.group) + await storyProject.addMetadata(file.tasks[0], 'story-id', storyId) + await storyProject.addMetadata(file.tasks[0], 'order', order) + }) } module.exports = CliController -// Helper Functions -newConfigFromFile = async (configPath) => { +async function addStoryTask(storyProject, storyId, description) { + const storyPath = path.join(storyProject.path, 'README.md') + const file = await storyProject.addTaskToFile({ path: storyPath, list: 'NOTE', tags: ['story'], content: description }) + file.rollback() + .extractTasks(storyProject.config) + await storyProject.addMetadata(file.tasks[0], 'order', 0) + await storyProject.addMetadata(file.tasks[0], 'story-id', storyId) +} + +async function createStoryProject(projectPath, storyId, log) { + const storyProjectPath = getStoryProjectPath(projectPath, storyId) + try { + await rm(storyProjectPath, { recursive: true }) + await mkdir(storyProjectPath, { recursive: true }) + } catch (e) { + log(`${e.code}: ${e.path}`) + } + const tasksDir = TASKS_DIR + const storyProject = await CliController.imdoneInit({ projectPath: storyProjectPath, tasksDir }) + return storyProject +} + +function getStoryProjectPath(projectPath, storyId) { + return path.join(projectPath, STORIES_DIR, storyId) +} + +async function newConfigFromFile(configPath) { const config = await readFile(configPath, 'utf8') return new Config(loadYAML(config)) } diff --git a/lib/cli/markdownStoryParser.js b/lib/cli/markdownStoryParser.js index 4d2daf3b..6b797876 100644 --- a/lib/cli/markdownStoryParser.js +++ b/lib/cli/markdownStoryParser.js @@ -74,5 +74,5 @@ module.exports = function parse(markdown) { tasks.push({ group, text: content.replace(/^\[.\]\s/, '') }) } }) - return { name: storyId, description, tasks } + return { storyId, description, tasks } } \ No newline at end of file diff --git a/lib/config.js b/lib/config.js index eaf1507b..12b999cc 100644 --- a/lib/config.js +++ b/lib/config.js @@ -30,6 +30,14 @@ class Config { this.settings.defaultFilter = filter } + get name() { + _get(this, 'settings.name', '') + } + + set name(name) { + this.settings.name = name + } + includeList(name) { return ( this.code && @@ -123,6 +131,10 @@ class Config { return _get(this, 'settings.journalPath', '') } + set journalPath(path) { + this.settings.journalPath = path + } + get journalTemplate() { const template = _get(this, 'settings.journalTemplate', '') return template === 'null' ? '' : template || '' diff --git a/lib/project.js b/lib/project.js index 9b1ae380..76019e08 100644 --- a/lib/project.js +++ b/lib/project.js @@ -15,6 +15,7 @@ const _isString = require('lodash.isstring') const exec = require('child_process').exec const fastSort = require('fast-sort/dist/sort.js') const Task = require('./task') +const eol = require('eol') function calculateTotals(lists) { const totals = {} @@ -344,14 +345,14 @@ module.exports = class WorkerProject extends Project { } newCard(opts = {}) { - let { list, path, template } = opts + let { list, path, template, title } = opts if (path) { path = this.getFullPath(path) if (!fileGateway.existsSync(path)) { fileGateway.writeFileSync(path, '') } } else { - path = this.getFullPath(this.getNewCardsFile()) + path = this.getFullPath(this.getNewCardsFile({title})) } const stat = fileGateway.statSync(path) if (!template) template = this.getNewCardTemplate(path, stat) @@ -367,8 +368,8 @@ module.exports = class WorkerProject extends Project { } async addTaskToFile({path, list, content, tags, contexts}) { - const cardData = this.newCard({list, path}) - const filePath = this.getNewCardsFile({title: content}) + const cardData = this.newCard({list, path, title: eol.split(content)[0]}) + const filePath = cardData.path if (tags) { let tagContent = '' tags.forEach(tag => { From e22137f47e82a79a86c193cd6ae1a6254d239539 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Thu, 7 Sep 2023 07:49:40 -0400 Subject: [PATCH 05/16] Clean up defaults --- cli.js | 27 +++++++++++++++++---------- lib/cli/CliControler.js | 5 +++-- lib/cli/markdownStoryParser.js | 2 +- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cli.js b/cli.js index ce74ae11..bf3cdd74 100755 --- a/cli.js +++ b/cli.js @@ -1,28 +1,35 @@ #!/usr/bin/env node const { program } = require('commander'); -const { imdoneInit, importMarkdown, addTask, listTasks } = require('./lib/cli/CliControler') +const { DEFAULT_CONFIG_PATH, imdoneInit, importMarkdown, addTask, listTasks } = require('./lib/cli/CliControler') const package = require('./package.json') const path = require('path') const { log, info, warn, logQueue } = hideLogs() +const DEFAULT_PROJECT_DIR = 'backlog' +const PROJECT_OPTION = '-p, --project-path ' +const PROJECT_OPTION_DESCRIPTION = 'The path to the imdone project' +const CONFIG_OPTION = '-c, --config-path ' +const CONFIG_OPTION_DESCRIPTION = 'The path to the imdone config file' + const defaultProjectPath = path.join(process.env.PWD, 'backlog') -// TODO ## Add an option to add properties/card.js + +// TODO ## Add an option to add properties and actions program .version(package.version, '-v, --version', 'output the current version') .command('init') .description('initialize imdone project') -.option('-p, --project-path ', 'The path to the imdone project') -.option('-c, --config-path ', 'The path to the imdone config file') +.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION, DEFAULT_PROJECT_DIR) +.option(CONFIG_OPTION, CONFIG_OPTION_DESCRIPTION, DEFAULT_CONFIG_PATH) .action(async function () { - let { projectPath = defaultProjectPath, configPath } = this.opts() + let { projectPath, configPath } = this.opts() await imdoneInit({projectPath, configPath}) }) program .command('import') .description('import markdown from STDIN') -.option('-p, --project-path ', 'The path to the imdone project') +.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION, DEFAULT_PROJECT_DIR) .action(async function () { let { projectPath = defaultProjectPath } = this.opts() const isTTY = process.stdin.isTTY; @@ -46,23 +53,23 @@ program program .command('add ') .description('add a task') -.option('-p, --project-path ', 'The path to the imdone project') +.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION, DEFAULT_PROJECT_DIR) .option('-l, --list ', 'The task list to use') .option('-t, --tags ', 'The tags to use') .option('-c, --contexts ', 'The contexts to use') .action(async function () { - let { projectPath = defaultProjectPath, list, tags, contexts } = this.opts() + let { projectPath, list, tags, contexts } = this.opts() await addTask({task: this.args[0], projectPath, list, tags, contexts, log}) }) program .command('ls') .description('list tasks') -.option('-p, --project-path ', 'The path to the imdone project') +.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION, DEFAULT_PROJECT_DIR) .option('-f, --filter ', 'The filter to use') .option('-j, --json', 'Output as json') .action(async function () { - let { projectPath = defaultProjectPath, filter, json } = this.opts() + let { projectPath, filter, json } = this.opts() await listTasks(projectPath, filter, json, log) }) program.parse(); diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index 0fefe148..b78fb8c3 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -15,6 +15,7 @@ const CARD_PROPERTIES_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone' const ACTIONS_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'actions') const CliController = {} +CliController.DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_PATH CliController.imdoneInit = async function ({projectPath, configPath = DEFAULT_CONFIG_PATH, tasksDir = STORIES_DIR}) { projectPath = resolve(projectPath) configPath = resolve(configPath) @@ -113,14 +114,14 @@ async function newConfigFromFile(configPath) { } function getGroupedTasks(tasks) { - const groupedTasks = {"Ungrouped Tasks": []} + const groupedTasks = {"Stories": []} tasks.forEach((task) => { const group = task.meta.group if (group) { if (!groupedTasks[group]) groupedTasks[group] = [] groupedTasks[group].push(task) } else { - groupedTasks["Ungrouped Tasks"].push(task) + groupedTasks["Stories"].push(task) } }) return groupedTasks diff --git a/lib/cli/markdownStoryParser.js b/lib/cli/markdownStoryParser.js index 6b797876..0397ace2 100644 --- a/lib/cli/markdownStoryParser.js +++ b/lib/cli/markdownStoryParser.js @@ -70,7 +70,7 @@ module.exports = function parse(markdown) { } if (inTasks && inBulletList && inListItem && inParagraph && type == inline) { - const group = headingLevel > 2 ? groupName : 'ungrouped' + const group = headingLevel > 2 ? groupName : 'Ungrouped Tasks' tasks.push({ group, text: content.replace(/^\[.\]\s/, '') }) } }) From 5e1310ff79e228ef6a3f8dc77454ad9342633e53 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Thu, 7 Sep 2023 15:07:16 -0400 Subject: [PATCH 06/16] Store session info and add completed tasks to DONE on import --- backlog/README.md | 27 ++++++++-- lib/cli/CliControler.js | 11 +++- lib/cli/StoryTaskSession.js | 52 +++++++++++++++++++ lib/cli/__tests__/markdownStoryParser.spec.js | 13 +++-- lib/cli/markdownStoryParser.js | 4 +- lib/project.js | 4 ++ lib/repository.js | 27 +++++----- 7 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 lib/cli/StoryTaskSession.js diff --git a/backlog/README.md b/backlog/README.md index 6d5f3367..2e0a95b4 100644 --- a/backlog/README.md +++ b/backlog/README.md @@ -6,13 +6,30 @@ This directory contains folders for each backlog item. ## Work on a story ### After collaborative design, import a story and story tasks from markdown ```bash -npx imdone import story < +npx imdone import <` should be the markdown title - [x] On import always remove the contents of the `backlog/story/` -- [ ] Shold handle a file with the following format +- [x] Shold handle a file with the following format +- [ ] Make sure checked items are put in DONE list ```markdown # @@ -28,16 +45,18 @@ This is the story summary - [ ] A task in a group ``` - [x] use `markdown-it.parse` to create AST -- [ ] Save story-id project path so it's available for starting a task, after a task is started, save the task-id +- [x] Save story-id project path so it's available for starting a task ### Day to day work after collaborative story design #### Start a task ```bash -npx imdone start -p backlog +npx imdone start ``` - [ ] This should find the task and create a branch off of main named `story////` - [ ] If the branch exists, check it out +- [ ] Set the task id in session so we know what to close +- [ ] Save the branch name in session so we can check it out again #### Add breadcrumbs for the next developer or ensemble 1. Open the file with the same name as the branch under backlog and add content!!! diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index b78fb8c3..0018d285 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -4,6 +4,12 @@ const { loadYAML } = require('../tools') const { readFile } = require('fs/promises') const Config = require('../config') const parseMarkdownStory = require('./markdownStoryParser') +const { + setProjectPath, + getProjectPath, + setTaskId, + getTaskId +} = require('./StoryTaskSession') const { mkdir, rm, cp } = require('fs/promises') const path = require('path') const eol = require('eol') @@ -66,11 +72,13 @@ CliController.importMarkdown = async function(projectPath, markdown, log) { const project = await CliController.imdoneInit({projectPath}) const { storyId, description, tasks } = parseMarkdownStory(markdown) const storyProject = await createStoryProject(projectPath, storyId, log) + setProjectPath(storyProject.path) await addStoryTask(storyProject, storyId, eol.split(description)[0], tasks, log) tasks.forEach(async (task, i) => { const order = (i + 1) * (10) - const file = await storyProject.addTaskToFile({list: 'TODO', tags: ['task'], content: task.text}) + const list = task.done ? 'DONE' : 'TODO' + const file = await storyProject.addTaskToFile({list, tags: ['task'], content: task.text}) file.rollback() .extractTasks(project.config) @@ -101,6 +109,7 @@ async function createStoryProject(projectPath, storyId, log) { } const tasksDir = TASKS_DIR const storyProject = await CliController.imdoneInit({ projectPath: storyProjectPath, tasksDir }) + storyProject.removeList('BACKLOG') return storyProject } diff --git a/lib/cli/StoryTaskSession.js b/lib/cli/StoryTaskSession.js new file mode 100644 index 00000000..b296630e --- /dev/null +++ b/lib/cli/StoryTaskSession.js @@ -0,0 +1,52 @@ +const path = require('path') +const {readFile, writeFile, mkdir, access} = require('fs/promises') +const homeDir = require('os').homedir() +const sessionPath = path.join(homeDir, '.imdone', 'session.json') + +async function ensureSessionFileExists() { + try { + await access(path.dirname(sessionPath)) + } catch (error) { + if (error.code === 'ENOENT') { + await mkdir(path.dirname(sessionPath), {recursive: true}) + await writeFile(sessionPath, '{}') + } + } +} + +async function getUserSession() { + await ensureSessionFileExists() + const session = await readFile(sessionPath, 'utf8') + return JSON.parse(session) +} + +async function saveUserSession(session) { + await ensureSessionFileExists() + await writeFile(sessionPath, JSON.stringify(session, null, 2)) +} + +const StoryTaskSession = {} + +StoryTaskSession.setProjectPath = async function (projectPath) { + const session = await getUserSession() + session.projectPath = projectPath + await saveUserSession(session) +} + +StoryTaskSession.getProjectPath = async function () { + const session = await getUserSession() + return session.projectPath +} + +StoryTaskSession.setTaskId = async function (taskId) { + const session = await getUserSession() + session.taskId = taskId + await saveUserSession(session) +} + +StoryTaskSession.getTaskId = async function () { + const session = await getUserSession() + return session.taskId +} + +module.exports = StoryTaskSession \ No newline at end of file diff --git a/lib/cli/__tests__/markdownStoryParser.spec.js b/lib/cli/__tests__/markdownStoryParser.spec.js index d1621723..1e894e6d 100644 --- a/lib/cli/__tests__/markdownStoryParser.spec.js +++ b/lib/cli/__tests__/markdownStoryParser.spec.js @@ -10,7 +10,7 @@ This is the story description. - [ ] An unfinished task ### Phase one (Interfaces) -- [ ] A task in phase one +- [x] A task in phase one - [ ] Another task in phase one - [ ] A sub task in phase one Some more data about the task @@ -18,19 +18,24 @@ This is the story description. ### Phase two (Implementation) - [ ] A task in phase two ` - const { name, description, tasks } = parse(markdown) - expect(name).to.equal('story-id') + const { storyId, description, tasks } = parse(markdown) + expect(storyId).to.equal('story-id') expect(description).to.equal('This is the story description.') expect(tasks.length).to.equal(5) expect(tasks[0].text).to.equal('An unfinished task') - expect(tasks[0].group).to.equal('ungrouped') + expect(tasks[0].group).to.equal('Ungrouped Tasks') + expect(tasks[0].done).to.equal(false) expect(tasks[1].text).to.equal('A task in phase one') expect(tasks[1].group).to.equal('Phase one (Interfaces)') + expect(tasks[1].done).to.equal(true) expect(tasks[2].text).to.equal('Another task in phase one') expect(tasks[2].group).to.equal('Phase one (Interfaces)') + expect(tasks[2].done).to.equal(false) expect(tasks[3].text).to.equal("A sub task in phase one\nSome more data about the task") expect(tasks[3].group).to.equal('Phase one (Interfaces)') + expect(tasks[3].done).to.equal(false) expect(tasks[4].text).to.equal('A task in phase two') expect(tasks[4].group).to.equal('Phase two (Implementation)') + expect(tasks[4].done).to.equal(false) }) }) \ No newline at end of file diff --git a/lib/cli/markdownStoryParser.js b/lib/cli/markdownStoryParser.js index 0397ace2..5d2131e7 100644 --- a/lib/cli/markdownStoryParser.js +++ b/lib/cli/markdownStoryParser.js @@ -71,7 +71,9 @@ module.exports = function parse(markdown) { if (inTasks && inBulletList && inListItem && inParagraph && type == inline) { const group = headingLevel > 2 ? groupName : 'Ungrouped Tasks' - tasks.push({ group, text: content.replace(/^\[.\]\s/, '') }) + const done = /^\[x\]\s/.test(content) + const text = content.replace(/^\[.\]\s/, '') + tasks.push({ group, text, done }) } }) return { storyId, description, tasks } diff --git a/lib/project.js b/lib/project.js index 76019e08..9426a971 100644 --- a/lib/project.js +++ b/lib/project.js @@ -171,6 +171,10 @@ module.exports = class WorkerProject extends Project { return this.getCards(this.filter) } + removeList(list) { + this.repo.removeList(list) + } + getLists(opts) { const {tasks = this.getDefaultFilteredCards(), populateFiltered = false} = opts || {} return Repository.getTasksByList(this.repo, tasks, true, populateFiltered) diff --git a/lib/repository.js b/lib/repository.js index ac2ac30c..4f2c1cfc 100644 --- a/lib/repository.js +++ b/lib/repository.js @@ -203,20 +203,21 @@ Repository.prototype.addList = function (list, cb) { } Repository.prototype.removeList = function (list, cb) { - cb = tools.cb(cb) - if (!this.listExists(list)) return cb() - var self = this - var fn = function (err) { - if (!err) self.emit('list.modified', list) - cb(err) - } + return new Promise((resolve, reject) => { + var fn = (err) => { + if (err) return cb && cb(err) || reject(err) + this.emit('list.modified', list) + resolve() + } + if (!this.listExists(list)) return fn() - var lists = _reject(this.getLists(), { name: list }) - if (this.config.code && this.config.code.include_lists) { - this.config.code.include_lists = _reject(this.config.code.include_lists, list) - } - this.setLists(lists) - this.saveConfig(fn) + var lists = _reject(this.getLists(), { name: list }) + if (this.config.code && this.config.code.include_lists) { + this.config.code.include_lists = _reject(this.config.code.include_lists, list) + } + this.setLists(lists) + this.saveConfig(fn) + }) } /** From 6530e50b14497129c5dba5dc31cc3310127bf41c Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sat, 9 Sep 2023 06:10:43 -0400 Subject: [PATCH 07/16] import story tasks from markdown --- .gitignore | 3 + backlog/README.md | 6 +- cli.js | 32 ++++--- devops/imdone/actions/board.js | 14 ++- devops/imdone/config.yml | 4 +- lib/cli/CliControler.js | 85 +++++++++++++------ lib/cli/StoryTaskSession.js | 11 +++ lib/cli/__tests__/markdownStoryParser.spec.js | 2 +- lib/project.js | 20 ++++- package-lock.json | 54 ++++++++++++ package.json | 1 + 11 files changed, 184 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index e8cc7606..056f60e5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ tmp .env .DS_Store debug.md + +backlog/.imdone +backlog/stories diff --git a/backlog/README.md b/backlog/README.md index 2e0a95b4..02c21e83 100644 --- a/backlog/README.md +++ b/backlog/README.md @@ -29,7 +29,7 @@ EOF - [x] `` should be the markdown title - [x] On import always remove the contents of the `backlog/story/` - [x] Shold handle a file with the following format -- [ ] Make sure checked items are put in DONE list +- [x] Make sure checked items are put in DONE list ```markdown # @@ -53,7 +53,8 @@ This is the story summary ```bash npx imdone start ``` -- [ ] This should find the task and create a branch off of main named `story////` +- [ ] This should find the task and create a branch named `story////` +- [ ] Move the task to the `DOING` list - [ ] If the branch exists, check it out - [ ] Set the task id in session so we know what to close - [ ] Save the branch name in session so we can check it out again @@ -73,6 +74,7 @@ npx imdone ls -p backlog -s ``` - [ ] `./backlog` is the default project folder - [ ] If the current branch starts with `story/`, parse the story id from the branch name +- [ ] If story from branch name isn't found and If the storyId is in the session use it - [ ] Can also pass in the story id with the `-s option - [ ] Can also use the filter option diff --git a/cli.js b/cli.js index bf3cdd74..6dd3cb4d 100755 --- a/cli.js +++ b/cli.js @@ -1,25 +1,28 @@ #!/usr/bin/env node const { program } = require('commander'); -const { DEFAULT_CONFIG_PATH, imdoneInit, importMarkdown, addTask, listTasks } = require('./lib/cli/CliControler') +const { + DEFAULT_CONFIG_PATH, + imdoneInit, + importMarkdown, + startTask, + addTask, + listTasks +} = require('./lib/cli/CliControler') const package = require('./package.json') const path = require('path') const { log, info, warn, logQueue } = hideLogs() -const DEFAULT_PROJECT_DIR = 'backlog' const PROJECT_OPTION = '-p, --project-path ' const PROJECT_OPTION_DESCRIPTION = 'The path to the imdone project' const CONFIG_OPTION = '-c, --config-path ' const CONFIG_OPTION_DESCRIPTION = 'The path to the imdone config file' -const defaultProjectPath = path.join(process.env.PWD, 'backlog') - -// TODO ## Add an option to add properties and actions program .version(package.version, '-v, --version', 'output the current version') .command('init') .description('initialize imdone project') -.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION, DEFAULT_PROJECT_DIR) +.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION) .option(CONFIG_OPTION, CONFIG_OPTION_DESCRIPTION, DEFAULT_CONFIG_PATH) .action(async function () { let { projectPath, configPath } = this.opts() @@ -29,9 +32,9 @@ program program .command('import') .description('import markdown from STDIN') -.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION, DEFAULT_PROJECT_DIR) +.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION) .action(async function () { - let { projectPath = defaultProjectPath } = this.opts() + let { projectPath } = this.opts() const isTTY = process.stdin.isTTY; const stdin = process.stdin; if (isTTY) return console.error('Markdown must be provided as stdin') @@ -49,11 +52,20 @@ program }); }) +program +.command('start ') +.description('start a task by id') +.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION) +.action(async function () { + const taskId = this.args[0] + let { projectPath } = this.opts() + await startTask(projectPath, taskId, log) +}) program .command('add ') .description('add a task') -.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION, DEFAULT_PROJECT_DIR) +.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION) .option('-l, --list ', 'The task list to use') .option('-t, --tags ', 'The tags to use') .option('-c, --contexts ', 'The contexts to use') @@ -65,7 +77,7 @@ program program .command('ls') .description('list tasks') -.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION, DEFAULT_PROJECT_DIR) +.option(PROJECT_OPTION, PROJECT_OPTION_DESCRIPTION) .option('-f, --filter ', 'The filter to use') .option('-j, --json', 'Output as json') .action(async function () { diff --git a/devops/imdone/actions/board.js b/devops/imdone/actions/board.js index a0b58f86..2090f6b7 100644 --- a/devops/imdone/actions/board.js +++ b/devops/imdone/actions/board.js @@ -1,10 +1,16 @@ module.exports = function (task) { const project = this.project - const groups = new Set(project.getCards().map((card) => card.meta.group && card.meta.group[0] || 'ungrouped')) - return [...groups].map((group) => { - const filterValue = encodeURIComponent(`allMeta.group = "${group}" or allTags = story`) + const groups = [...new Set( + project.getCards('meta.group = *').map((card) => card.meta.group && card.meta.group[0]) + )].map(group => { + const name = group + const value = `"${group}"` + return { name, value} + }) + return [{name: 'All tasks', value: '*'}, ...groups].map((group) => { + const filterValue = encodeURIComponent(`meta.group = ${group.value} or tags = story`) return { - title: group, + title: group.name, action: function () { project.openUrl(`imdone://active.repo?filter=${filterValue}`) } diff --git a/devops/imdone/config.yml b/devops/imdone/config.yml index 703e174a..71c77b75 100644 --- a/devops/imdone/config.yml +++ b/devops/imdone/config.yml @@ -31,7 +31,7 @@ lists: name: DOING id: jea6phpllxpsj7k - hidden: false - ignore: true + ignore: false name: DONE id: jea6phpllxpsj7l settings: @@ -53,7 +53,7 @@ settings: colors: [] template: | - + trackChanges: false metaNewLine: false addCompletedMeta: false diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index 0018d285..15270601 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -1,58 +1,58 @@ -const { resolve } = require('path') -const { createFileSystemProject } = require('../project-factory') +const { resolve } = path = require('path') +const eol = require('eol') +const { readFile, mkdir, rm, cp } = require('fs/promises') +const {simpleGit} = require('simple-git') const { loadYAML } = require('../tools') -const { readFile } = require('fs/promises') const Config = require('../config') -const parseMarkdownStory = require('./markdownStoryParser') +const { createFileSystemProject } = require('../project-factory') +const parseMarkdownStory = require('./MarkdownStoryParser') const { setProjectPath, getProjectPath, setTaskId, - getTaskId + getTaskId, + setStoryId, + getStoryId } = require('./StoryTaskSession') -const { mkdir, rm, cp } = require('fs/promises') -const path = require('path') -const eol = require('eol') +const DEFAULT_PROJECT_DIR = 'backlog' const STORIES_DIR = 'stories' const TASKS_DIR = 'tasks' +const TASK_ID = 'task-id' const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'config.yml') const CARD_PROPERTIES_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'properties') const ACTIONS_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'actions') const CliController = {} +const defaultProjectPath = path.join(process.env.PWD, DEFAULT_PROJECT_DIR) + +CliController.DEFAULT_PROJECT_DIR = DEFAULT_PROJECT_DIR CliController.DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_PATH -CliController.imdoneInit = async function ({projectPath, configPath = DEFAULT_CONFIG_PATH, tasksDir = STORIES_DIR}) { +CliController.imdoneInit = async function ({projectPath = defaultProjectPath, configPath = DEFAULT_CONFIG_PATH, tasksDir = STORIES_DIR}) { projectPath = resolve(projectPath) configPath = resolve(configPath) - const config = await newConfigFromFile(configPath) - config.name = path.basename(projectPath) - config.journalPath = tasksDir - await cp(CARD_PROPERTIES_PATH, path.join(projectPath, '.imdone', 'properties'), { recursive: true }) await cp(ACTIONS_PATH, path.join(projectPath, '.imdone', 'actions'), { recursive: true }) await mkdir(path.join(projectPath, tasksDir), {recursive: true}) - const project = createFileSystemProject({path: projectPath, config}) - await project.init() - return project + const config = await newConfigFromFile(configPath) + config.name = path.basename(projectPath) + config.journalPath = tasksDir + + return await init(projectPath, config) } -CliController.addTask = async function ({task, projectPath, list, tags, contexts, log}) { - projectPath = resolve(projectPath) - const project = createFileSystemProject({path: projectPath}) - await project.init() +CliController.addTask = async function ({task, projectPath = defaultProjectPath, list, tags, contexts, log}) { + const project = await init(projectPath) const file = await project.addTaskToFile({list, content: task, tags, contexts}) file.rollback() .extractTasks(project.config) - log(file.tasks[0].meta.sid[0]) + log(file.tasks[0].meta[TASK_ID][0]) } -CliController.listTasks = async function (projectPath, filter, json, log) { - projectPath = resolve(projectPath) - const project = createFileSystemProject({path: projectPath}) - await project.init() +CliController.listTasks = async function (projectPath = defaultProjectPath, filter, json, log) { + const project = await init(projectPath) const tasks = project.getCards(filter) const lists = project.getLists({tasks}) @@ -68,7 +68,7 @@ CliController.listTasks = async function (projectPath, filter, json, log) { } } -CliController.importMarkdown = async function(projectPath, markdown, log) { +CliController.importMarkdown = async function(projectPath = defaultProjectPath, markdown, log) { const project = await CliController.imdoneInit({projectPath}) const { storyId, description, tasks } = parseMarkdownStory(markdown) const storyProject = await createStoryProject(projectPath, storyId, log) @@ -88,8 +88,41 @@ CliController.importMarkdown = async function(projectPath, markdown, log) { }) } +// #### Start a task +// ```bash +// npx imdone start +// ``` +// - [ ] This should find the task and create a branch named `story////` +// - [x] Move the task to the `DOING` list +// - [ ] If the branch exists, check it out +// - [ ] Set the task id in session so we know what to close +// - [ ] Save the branch name in session so we can check it out again + +CliController.startTask = async function (projectPath = defaultProjectPath, taskId, log) { + const project = await init(projectPath) + const task = project.getCards().find(({meta}) => meta[TASK_ID] && meta[TASK_ID][0] === taskId) + const storyId = task.meta['story-id'][0] + + await setProjectPath(getStoryProjectPath(projectPath, storyId)) + await setTaskId(taskId) + await setStoryId(storyId) + const taskName = project.sanitizeFileName(task.text) + const branchName = `story/${storyId}/task/${taskName}` + + project.moveTask(task, 'DOING', 0) + log(`git checkout -b ${branchName}`) + await simpleGit().checkoutBranch(branchName) +} + module.exports = CliController +async function init(projectPath, config) { + projectPath = resolve(projectPath) + const project = createFileSystemProject({path: projectPath, config}) + await project.init() + return project +} + async function addStoryTask(storyProject, storyId, description) { const storyPath = path.join(storyProject.path, 'README.md') const file = await storyProject.addTaskToFile({ path: storyPath, list: 'NOTE', tags: ['story'], content: description }) diff --git a/lib/cli/StoryTaskSession.js b/lib/cli/StoryTaskSession.js index b296630e..aa5f651a 100644 --- a/lib/cli/StoryTaskSession.js +++ b/lib/cli/StoryTaskSession.js @@ -49,4 +49,15 @@ StoryTaskSession.getTaskId = async function () { return session.taskId } +StoryTaskSession.setStoryId = async function (storyId) { + const session = await getUserSession() + session.storyId = storyId + await saveUserSession(session) +} + +StoryTaskSession.getStoryId = async function () { + const session = await getUserSession() + return session.storyId +} + module.exports = StoryTaskSession \ No newline at end of file diff --git a/lib/cli/__tests__/markdownStoryParser.spec.js b/lib/cli/__tests__/markdownStoryParser.spec.js index 1e894e6d..56bb26dc 100644 --- a/lib/cli/__tests__/markdownStoryParser.spec.js +++ b/lib/cli/__tests__/markdownStoryParser.spec.js @@ -1,4 +1,4 @@ -const parse = require("../markdownStoryParser") +const parse = require("../MarkdownStoryParser") const expect = require("chai").expect describe('markdownStoryParser', () => { it('should parse a markdown story name', () => { diff --git a/lib/project.js b/lib/project.js index 9426a971..ad131d0d 100644 --- a/lib/project.js +++ b/lib/project.js @@ -301,6 +301,15 @@ module.exports = class WorkerProject extends Project { return this.updateCardContent(task, content) } + async moveTask(task, newList, newPos) { + return new Promise((resolve, reject) => { + this.repo.moveTask({task, newList, newPos}, (err) => { + if (err) return reject(err) + resolve() + }) + }) + } + async updateCardContent(task, content) { return new Promise((resolve, reject) => { this.repo.modifyTaskFromContent(task, content, (err) => { @@ -464,9 +473,7 @@ module.exports = class WorkerProject extends Project { return this.getJournalFile().fullFilePath if (journalType === JOURNAL_TYPE.NEW_FILE) { if (!title) return this.getFullPath(this.config.journalPath) - let fileName = `${sanitize(removeMD(title))}.md` - if (this.config.replaceSpacesWith) - fileName = fileName.replace(/ /g, this.config.replaceSpacesWith) + const fileName = `${this.sanitizeFileName(title)}.md` const fileFolder = this.getFullPath(this.config.journalPath) const filePath = _path.join(fileFolder, fileName) @@ -479,6 +486,13 @@ module.exports = class WorkerProject extends Project { } } + sanitizeFileName(name) { + let fileName = sanitize(removeMD(name)) + if (this.config.replaceSpacesWith) + fileName = fileName.replace(/ /g, this.config.replaceSpacesWith) + return fileName + } + getJournalFile() { const month = moment().format('YYYY-MM') const today = moment().format('YYYY-MM-DD') diff --git a/package-lock.json b/package-lock.json index 1d25597c..07de743c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,9 +66,13 @@ "sane": "^4.1.0", "sanitize-filename": "^1.6.3", "sift": "^13.4.0", + "simple-git": "^3.19.1", "uniqid": "^5.4.0", "xregexp": "^5.1.0" }, + "bin": { + "imdone": "cli.js" + }, "devDependencies": { "chai": "^4.3.6", "colors": "^1.4.0", @@ -628,6 +632,19 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, "node_modules/@kwvanderlinde/markdown-it-wikilinks": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@kwvanderlinde/markdown-it-wikilinks/-/markdown-it-wikilinks-1.0.2.tgz", @@ -6277,6 +6294,20 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-git": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.19.1.tgz", + "integrity": "sha512-Ck+rcjVaE1HotraRAS8u/+xgTvToTuoMkT9/l9lvuP5jftwnYUp6DwuJzsKErHgfyRk8IB8pqGHWEbM3tLgV1w==", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/sinon": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.2.tgz", @@ -7941,6 +7972,19 @@ } } }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "requires": { + "debug": "^4.1.1" + } + }, + "@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, "@kwvanderlinde/markdown-it-wikilinks": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@kwvanderlinde/markdown-it-wikilinks/-/markdown-it-wikilinks-1.0.2.tgz", @@ -12300,6 +12344,16 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "simple-git": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.19.1.tgz", + "integrity": "sha512-Ck+rcjVaE1HotraRAS8u/+xgTvToTuoMkT9/l9lvuP5jftwnYUp6DwuJzsKErHgfyRk8IB8pqGHWEbM3tLgV1w==", + "requires": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.4" + } + }, "sinon": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.2.tgz", diff --git a/package.json b/package.json index 2a32d368..1698736c 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "sane": "^4.1.0", "sanitize-filename": "^1.6.3", "sift": "^13.4.0", + "simple-git": "^3.19.1", "uniqid": "^5.4.0", "xregexp": "^5.1.0" }, From 02d13835f33ff612f5f23aad3f7a2be77dff1018 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sat, 9 Sep 2023 17:12:28 -0400 Subject: [PATCH 08/16] Checkout branch --- lib/cli/CliControler.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index 15270601..deab0d11 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -22,7 +22,9 @@ const TASK_ID = 'task-id' const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'config.yml') const CARD_PROPERTIES_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'properties') const ACTIONS_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'actions') -const CliController = {} +module.exports = CliController = { + git: simpleGit, +} const defaultProjectPath = path.join(process.env.PWD, DEFAULT_PROJECT_DIR) @@ -111,11 +113,13 @@ CliController.startTask = async function (projectPath = defaultProjectPath, task project.moveTask(task, 'DOING', 0) log(`git checkout -b ${branchName}`) - await simpleGit().checkoutBranch(branchName) + const branch = await CliController.git(process.env.PWD).branchLocal() + log('currentBranch', branch.current) + if (branch.current !== branchName) { + await CliController.git(process.env.PWD).checkoutBranch(branchName, 'HEAD') + } } -module.exports = CliController - async function init(projectPath, config) { projectPath = resolve(projectPath) const project = createFileSystemProject({path: projectPath, config}) From 39a7a9b1b9bd30a051416e995e1020d45d7162c7 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sat, 9 Sep 2023 17:27:16 -0400 Subject: [PATCH 09/16] checkout and pull --- lib/cli/CliControler.js | 44 +++++++++++++++++++++++-------------- lib/cli/StoryTaskSession.js | 10 +++++++++ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index deab0d11..0221fe41 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -12,13 +12,19 @@ const { setTaskId, getTaskId, setStoryId, - getStoryId + getStoryId, + setBranchName } = require('./StoryTaskSession') const DEFAULT_PROJECT_DIR = 'backlog' const STORIES_DIR = 'stories' const TASKS_DIR = 'tasks' const TASK_ID = 'task-id' +const STORY_ID = 'story-id' +const TODO = 'TODO' +const DOING = 'DOING' +const DONE = 'DONE' +const ORDER = 'order' const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'config.yml') const CARD_PROPERTIES_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'properties') const ACTIONS_PATH = path.join(__dirname, '..', '..', 'devops', 'imdone', 'actions') @@ -79,14 +85,14 @@ CliController.importMarkdown = async function(projectPath = defaultProjectPath, tasks.forEach(async (task, i) => { const order = (i + 1) * (10) - const list = task.done ? 'DONE' : 'TODO' + const list = task.done ? DONE : TODO const file = await storyProject.addTaskToFile({list, tags: ['task'], content: task.text}) file.rollback() .extractTasks(project.config) await storyProject.addMetadata(file.tasks[0], 'group', task.group) - await storyProject.addMetadata(file.tasks[0], 'story-id', storyId) - await storyProject.addMetadata(file.tasks[0], 'order', order) + await storyProject.addMetadata(file.tasks[0], STORY_ID, storyId) + await storyProject.addMetadata(file.tasks[0], ORDER, order) }) } @@ -96,28 +102,34 @@ CliController.importMarkdown = async function(projectPath = defaultProjectPath, // ``` // - [ ] This should find the task and create a branch named `story////` // - [x] Move the task to the `DOING` list -// - [ ] If the branch exists, check it out -// - [ ] Set the task id in session so we know what to close -// - [ ] Save the branch name in session so we can check it out again - +// - [x] If the branch exists, check it out +// - [x] Set the task id in session so we know what to close +// - [x] Save the branch name in session so we can check it out again +// - [ ] Pull the branch if it exists CliController.startTask = async function (projectPath = defaultProjectPath, taskId, log) { const project = await init(projectPath) const task = project.getCards().find(({meta}) => meta[TASK_ID] && meta[TASK_ID][0] === taskId) - const storyId = task.meta['story-id'][0] + const storyId = task.meta[STORY_ID][0] + const taskName = project.sanitizeFileName(task.text) + const branchName = `story/${storyId}/task/${taskName}` await setProjectPath(getStoryProjectPath(projectPath, storyId)) await setTaskId(taskId) await setStoryId(storyId) - const taskName = project.sanitizeFileName(task.text) - const branchName = `story/${storyId}/task/${taskName}` + await setBranchName(branchName) - project.moveTask(task, 'DOING', 0) + project.moveTask(task, DOING, 0) log(`git checkout -b ${branchName}`) - const branch = await CliController.git(process.env.PWD).branchLocal() + + const git = CliController.git() + const branch = await git.branchLocal() log('currentBranch', branch.current) if (branch.current !== branchName) { - await CliController.git(process.env.PWD).checkoutBranch(branchName, 'HEAD') + // await git.checkoutBranch(branchName, 'HEAD') + } else { + await git.pull() } + } async function init(projectPath, config) { @@ -132,8 +144,8 @@ async function addStoryTask(storyProject, storyId, description) { const file = await storyProject.addTaskToFile({ path: storyPath, list: 'NOTE', tags: ['story'], content: description }) file.rollback() .extractTasks(storyProject.config) - await storyProject.addMetadata(file.tasks[0], 'order', 0) - await storyProject.addMetadata(file.tasks[0], 'story-id', storyId) + await storyProject.addMetadata(file.tasks[0], ORDER, 0) + await storyProject.addMetadata(file.tasks[0], STORY_ID, storyId) } async function createStoryProject(projectPath, storyId, log) { diff --git a/lib/cli/StoryTaskSession.js b/lib/cli/StoryTaskSession.js index aa5f651a..a2e7a7d9 100644 --- a/lib/cli/StoryTaskSession.js +++ b/lib/cli/StoryTaskSession.js @@ -60,4 +60,14 @@ StoryTaskSession.getStoryId = async function () { return session.storyId } +StoryTaskSession.setBranchName = async function (branchName) { + const session = await getUserSession() + session.branchName = branchName +} + +StoryTaskSession.getBranchName = async function () { + const session = await getUserSession() + return session.branchName +} + module.exports = StoryTaskSession \ No newline at end of file From 919cc660c3307cabb765d6983711edca70e668ed Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sat, 9 Sep 2023 17:28:14 -0400 Subject: [PATCH 10/16] Always pull --- lib/cli/CliControler.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index 0221fe41..6828562f 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -125,11 +125,9 @@ CliController.startTask = async function (projectPath = defaultProjectPath, task const branch = await git.branchLocal() log('currentBranch', branch.current) if (branch.current !== branchName) { - // await git.checkoutBranch(branchName, 'HEAD') - } else { - await git.pull() + await git.checkoutBranch(branchName, 'HEAD') } - + await git.pull() } async function init(projectPath, config) { From f2673f80efb86ca2c18cb6498fb655557d3a8c41 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sat, 9 Sep 2023 17:33:36 -0400 Subject: [PATCH 11/16] Checkout local branch if it exists --- lib/cli/CliControler.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index 6828562f..d4c1af45 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -122,10 +122,12 @@ CliController.startTask = async function (projectPath = defaultProjectPath, task log(`git checkout -b ${branchName}`) const git = CliController.git() - const branch = await git.branchLocal() - log('currentBranch', branch.current) - if (branch.current !== branchName) { - await git.checkoutBranch(branchName, 'HEAD') + const branches = await git.branchLocal() + log('currentBranch', branches.current) + log('branches', branches) + if (branches.current !== branchName) { + if (!branches.all.includes(branchName)) await git.checkoutBranch(branchName, 'HEAD') + else await git.checkoutLocalBranch(branchName) } await git.pull() } From 73055c5debcdb699a6538b9936e34fe72e4c4e87 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sat, 9 Sep 2023 21:27:25 -0400 Subject: [PATCH 12/16] Use checkout when branch exists --- lib/cli/CliControler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index d4c1af45..c9cf0d96 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -126,8 +126,8 @@ CliController.startTask = async function (projectPath = defaultProjectPath, task log('currentBranch', branches.current) log('branches', branches) if (branches.current !== branchName) { - if (!branches.all.includes(branchName)) await git.checkoutBranch(branchName, 'HEAD') - else await git.checkoutLocalBranch(branchName) + if (branches.all.includes(branchName)) await git.checkoutBranch(branchName, 'HEAD') + else await git.checkout(branchName) } await git.pull() } From 72ad5b88234a41dbb31849b11347a16ef2021aed Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sat, 9 Sep 2023 21:28:00 -0400 Subject: [PATCH 13/16] Use checkout when branch exists --- lib/cli/CliControler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index c9cf0d96..555df8f7 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -126,7 +126,7 @@ CliController.startTask = async function (projectPath = defaultProjectPath, task log('currentBranch', branches.current) log('branches', branches) if (branches.current !== branchName) { - if (branches.all.includes(branchName)) await git.checkoutBranch(branchName, 'HEAD') + if (!branches.all.includes(branchName)) await git.checkoutBranch(branchName, 'HEAD') else await git.checkout(branchName) } await git.pull() From af77e5d344b23f7731c22acc9ed7805fc4b7f916 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sat, 9 Sep 2023 21:52:37 -0400 Subject: [PATCH 14/16] try/catch on pull --- lib/cli/CliControler.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index 555df8f7..e8bb4b08 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -129,7 +129,11 @@ CliController.startTask = async function (projectPath = defaultProjectPath, task if (!branches.all.includes(branchName)) await git.checkoutBranch(branchName, 'HEAD') else await git.checkout(branchName) } - await git.pull() + try { + await git.pull() + } catch (e) { + log(e) + } } async function init(projectPath, config) { From 48e0aec77e50414c1364c0fdda59d01bfae05546 Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sun, 10 Sep 2023 21:57:21 -0400 Subject: [PATCH 15/16] Fetch first --- lib/cli/CliControler.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index e8bb4b08..bf00f1d0 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -100,7 +100,7 @@ CliController.importMarkdown = async function(projectPath = defaultProjectPath, // ```bash // npx imdone start // ``` -// - [ ] This should find the task and create a branch named `story////` +// - [x] This should find the task and create a branch named `story////` // - [x] Move the task to the `DOING` list // - [x] If the branch exists, check it out // - [x] Set the task id in session so we know what to close @@ -119,12 +119,11 @@ CliController.startTask = async function (projectPath = defaultProjectPath, task await setBranchName(branchName) project.moveTask(task, DOING, 0) - log(`git checkout -b ${branchName}`) const git = CliController.git() + await git.fetch() const branches = await git.branchLocal() - log('currentBranch', branches.current) - log('branches', branches) + if (branches.current !== branchName) { if (!branches.all.includes(branchName)) await git.checkoutBranch(branchName, 'HEAD') else await git.checkout(branchName) From 9de3a93d89c4550c5ff9dd4db61c6b6d0ab4ba8a Mon Sep 17 00:00:00 2001 From: Jesse Piascik Date: Sun, 10 Sep 2023 22:04:05 -0400 Subject: [PATCH 16/16] Pull if there's a remote branch --- lib/cli/CliControler.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/cli/CliControler.js b/lib/cli/CliControler.js index bf00f1d0..b1312ea9 100644 --- a/lib/cli/CliControler.js +++ b/lib/cli/CliControler.js @@ -128,11 +128,9 @@ CliController.startTask = async function (projectPath = defaultProjectPath, task if (!branches.all.includes(branchName)) await git.checkoutBranch(branchName, 'HEAD') else await git.checkout(branchName) } - try { - await git.pull() - } catch (e) { - log(e) - } + + const remoteBranches = await git.branch(['-r']) + if (remoteBranches.all.includes(`origin/${branchName}`)) await git.pull() } async function init(projectPath, config) {