Add ProjectDirs extension pattern and injectable clock
- core: Add ProjectDirs helper class on ProjectContext (context.dirs) with project getter for .project/ dir; feature packages extend it via extension methods to expose their own dirs - kanban: Add KanbanDirs extension on ProjectDirs exposing .kanban; replace all 14 p.join(context.root, '.project', 'kanban') call sites with context.dirs.kanban; drop now-unused path imports - kanban: Add clock: DateTime Function() field to TicketStore (defaults to DateTime.now); use clock().toUtc() in create() for deterministic timestamps in tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
8d787235b9
commit
53f9493364
17 changed files with 43 additions and 29 deletions
|
|
@ -14,6 +14,18 @@ class DewConfig {
|
||||||
factory DewConfig.fromYaml(YamlMap yaml) => DewConfig(raw: yaml);
|
factory DewConfig.fromYaml(YamlMap yaml) => DewConfig(raw: yaml);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Path helper for well-known directories under a project root.
|
||||||
|
///
|
||||||
|
/// Feature packages extend this via extension methods to expose their own
|
||||||
|
/// directories (e.g. [KanbanDirs.kanban]).
|
||||||
|
class ProjectDirs {
|
||||||
|
final String _root;
|
||||||
|
const ProjectDirs(this._root);
|
||||||
|
|
||||||
|
/// `.project/` directory.
|
||||||
|
String get project => p.join(_root, '.project');
|
||||||
|
}
|
||||||
|
|
||||||
/// Locates the nearest project root and exposes the parsed [DewConfig].
|
/// Locates the nearest project root and exposes the parsed [DewConfig].
|
||||||
class ProjectContext {
|
class ProjectContext {
|
||||||
final String root;
|
final String root;
|
||||||
|
|
@ -22,6 +34,9 @@ class ProjectContext {
|
||||||
|
|
||||||
const ProjectContext({required this.root, required this.config, required this.fs});
|
const ProjectContext({required this.root, required this.config, required this.fs});
|
||||||
|
|
||||||
|
/// Typed path helpers for this project's well-known directories.
|
||||||
|
ProjectDirs get dirs => ProjectDirs(root);
|
||||||
|
|
||||||
/// Walks up from [from] (defaults to [fs.currentDirectory]) until a
|
/// Walks up from [from] (defaults to [fs.currentDirectory]) until a
|
||||||
/// `.project/dew.yaml` is found.
|
/// `.project/dew.yaml` is found.
|
||||||
static Future<ProjectContext> find({
|
static Future<ProjectContext> find({
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -31,7 +30,7 @@ class AddCommentCommand extends DewCommand with DewToolCommand {
|
||||||
|
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: context.config.kanban.prefix,
|
prefix: context.config.kanban.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import '../kanban_config.dart';
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ class ArchiveCommand extends DewCommand with DewToolCommand {
|
||||||
|
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final config = context.config.kanban;
|
final config = context.config.kanban;
|
||||||
final kanbanDir = p.join(context.root, '.project', 'kanban');
|
final kanbanDir = context.dirs.kanban;
|
||||||
|
|
||||||
final store = TicketStore(kanbanDir: kanbanDir, prefix: config.prefix, fs: context.fs);
|
final store = TicketStore(kanbanDir: kanbanDir, prefix: config.prefix, fs: context.fs);
|
||||||
final ticket = await store.findById(id);
|
final ticket = await store.findById(id);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket.dart';
|
import '../ticket.dart';
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
@ -35,7 +34,7 @@ class BoardCommand extends DewCommand with DewToolCommand {
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final config = context.config.kanban;
|
final config = context.config.kanban;
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: config.prefix,
|
prefix: config.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -67,7 +66,7 @@ class CreateCommand extends DewCommand with DewToolCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: config.prefix,
|
prefix: config.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -32,7 +31,7 @@ class DeleteCommand extends DewCommand with DewToolCommand {
|
||||||
final id = (args['id'] as String).toUpperCase();
|
final id = (args['id'] as String).toUpperCase();
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: context.config.kanban.prefix,
|
prefix: context.config.kanban.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket.dart';
|
import '../ticket.dart';
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
@ -33,7 +32,7 @@ class GetCommand extends DewCommand with DewToolCommand {
|
||||||
final id = (args['id'] as String).toUpperCase();
|
final id = (args['id'] as String).toUpperCase();
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: context.config.kanban.prefix,
|
prefix: context.config.kanban.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket.dart';
|
import '../ticket.dart';
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
@ -44,7 +43,7 @@ class LinkCommand extends DewCommand with DewToolCommand {
|
||||||
|
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: context.config.kanban.prefix,
|
prefix: context.config.kanban.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket.dart';
|
import '../ticket.dart';
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
@ -38,7 +37,7 @@ class ListCommand extends DewCommand with DewToolCommand {
|
||||||
|
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: context.config.kanban.prefix,
|
prefix: context.config.kanban.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -40,7 +39,7 @@ class MoveCommand extends DewCommand with DewToolCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: config.prefix,
|
prefix: config.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -44,7 +43,7 @@ class SearchCommand extends DewCommand with DewToolCommand {
|
||||||
|
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: context.config.kanban.prefix,
|
prefix: context.config.kanban.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -24,7 +23,7 @@ class StatsCommand extends DewCommand with DewToolCommand {
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final config = context.config.kanban;
|
final config = context.config.kanban;
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: config.prefix,
|
prefix: config.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import '../kanban_config.dart';
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ class UnarchiveCommand extends DewCommand with DewToolCommand {
|
||||||
|
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final config = context.config.kanban;
|
final config = context.config.kanban;
|
||||||
final kanbanDir = p.join(context.root, '.project', 'kanban');
|
final kanbanDir = context.dirs.kanban;
|
||||||
|
|
||||||
final store = TicketStore(kanbanDir: kanbanDir, prefix: config.prefix, fs: context.fs);
|
final store = TicketStore(kanbanDir: kanbanDir, prefix: config.prefix, fs: context.fs);
|
||||||
final ticket = await store.findById(id);
|
final ticket = await store.findById(id);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -31,7 +30,7 @@ class UnlinkCommand extends DewCommand with DewToolCommand {
|
||||||
|
|
||||||
final context = await ProjectContext.find(fs: _fs);
|
final context = await ProjectContext.find(fs: _fs);
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: context.config.kanban.prefix,
|
prefix: context.config.kanban.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import '../kanban_config.dart';
|
import '../kanban_config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
import '../ticket_store.dart';
|
import '../ticket_store.dart';
|
||||||
|
|
||||||
|
|
@ -79,7 +78,7 @@ class UpdateCommand extends DewCommand with DewToolCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
final store = TicketStore(
|
final store = TicketStore(
|
||||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
kanbanDir: context.dirs.kanban,
|
||||||
prefix: config.prefix,
|
prefix: config.prefix,
|
||||||
fs: context.fs,
|
fs: context.fs,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:yaml/yaml.dart';
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
class TicketTypeConfig {
|
class TicketTypeConfig {
|
||||||
|
|
@ -63,3 +64,9 @@ extension KanbanDewConfig on DewConfig {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extends [ProjectDirs] with the kanban board directory.
|
||||||
|
extension KanbanDirs on ProjectDirs {
|
||||||
|
/// Absolute path to `.project/kanban/`.
|
||||||
|
String get kanban => p.join(project, 'kanban');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,14 @@ class TicketStore {
|
||||||
final String prefix;
|
final String prefix;
|
||||||
final FileSystem fs;
|
final FileSystem fs;
|
||||||
|
|
||||||
|
/// Provides the current time for ticket creation. Injectable for testing.
|
||||||
|
final DateTime Function() clock;
|
||||||
|
|
||||||
const TicketStore({
|
const TicketStore({
|
||||||
required this.kanbanDir,
|
required this.kanbanDir,
|
||||||
required this.prefix,
|
required this.prefix,
|
||||||
this.fs = const LocalFileSystem(),
|
this.fs = const LocalFileSystem(),
|
||||||
|
this.clock = DateTime.now,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<Ticket> create({
|
Future<Ticket> create({
|
||||||
|
|
@ -31,7 +35,7 @@ class TicketStore {
|
||||||
title: title,
|
title: title,
|
||||||
type: type,
|
type: type,
|
||||||
column: column,
|
column: column,
|
||||||
created: DateTime.now().toUtc(),
|
created: clock().toUtc(),
|
||||||
body: body,
|
body: body,
|
||||||
comments: const [],
|
comments: const [],
|
||||||
milestones: milestones,
|
milestones: milestones,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue