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);
|
||||
}
|
||||
|
||||
/// 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].
|
||||
class ProjectContext {
|
||||
final String root;
|
||||
|
|
@ -22,6 +34,9 @@ class ProjectContext {
|
|||
|
||||
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
|
||||
/// `.project/dew.yaml` is found.
|
||||
static Future<ProjectContext> find({
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -31,7 +30,7 @@ class AddCommentCommand extends DewCommand with DewToolCommand {
|
|||
|
||||
final context = await ProjectContext.find(fs: _fs);
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: context.config.kanban.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:dew_core/dew_core.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../kanban_config.dart';
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ class ArchiveCommand extends DewCommand with DewToolCommand {
|
|||
|
||||
final context = await ProjectContext.find(fs: _fs);
|
||||
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 ticket = await store.findById(id);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket.dart';
|
||||
import '../ticket_store.dart';
|
||||
|
|
@ -35,7 +34,7 @@ class BoardCommand extends DewCommand with DewToolCommand {
|
|||
final context = await ProjectContext.find(fs: _fs);
|
||||
final config = context.config.kanban;
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: config.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -67,7 +66,7 @@ class CreateCommand extends DewCommand with DewToolCommand {
|
|||
}
|
||||
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: config.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ class DeleteCommand extends DewCommand with DewToolCommand {
|
|||
final id = (args['id'] as String).toUpperCase();
|
||||
final context = await ProjectContext.find(fs: _fs);
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: context.config.kanban.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket.dart';
|
||||
import '../ticket_store.dart';
|
||||
|
|
@ -33,7 +32,7 @@ class GetCommand extends DewCommand with DewToolCommand {
|
|||
final id = (args['id'] as String).toUpperCase();
|
||||
final context = await ProjectContext.find(fs: _fs);
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: context.config.kanban.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket.dart';
|
||||
import '../ticket_store.dart';
|
||||
|
|
@ -44,7 +43,7 @@ class LinkCommand extends DewCommand with DewToolCommand {
|
|||
|
||||
final context = await ProjectContext.find(fs: _fs);
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: context.config.kanban.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket.dart';
|
||||
import '../ticket_store.dart';
|
||||
|
|
@ -38,7 +37,7 @@ class ListCommand extends DewCommand with DewToolCommand {
|
|||
|
||||
final context = await ProjectContext.find(fs: _fs);
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: context.config.kanban.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -40,7 +39,7 @@ class MoveCommand extends DewCommand with DewToolCommand {
|
|||
}
|
||||
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: config.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -44,7 +43,7 @@ class SearchCommand extends DewCommand with DewToolCommand {
|
|||
|
||||
final context = await ProjectContext.find(fs: _fs);
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: context.config.kanban.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ class StatsCommand extends DewCommand with DewToolCommand {
|
|||
final context = await ProjectContext.find(fs: _fs);
|
||||
final config = context.config.kanban;
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: config.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:dew_core/dew_core.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../kanban_config.dart';
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ class UnarchiveCommand extends DewCommand with DewToolCommand {
|
|||
|
||||
final context = await ProjectContext.find(fs: _fs);
|
||||
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 ticket = await store.findById(id);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -31,7 +30,7 @@ class UnlinkCommand extends DewCommand with DewToolCommand {
|
|||
|
||||
final context = await ProjectContext.find(fs: _fs);
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: context.config.kanban.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:dew_core/dew_core.dart';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import '../kanban_config.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../ticket_store.dart';
|
||||
|
||||
|
|
@ -79,7 +78,7 @@ class UpdateCommand extends DewCommand with DewToolCommand {
|
|||
}
|
||||
|
||||
final store = TicketStore(
|
||||
kanbanDir: p.join(context.root, '.project', 'kanban'),
|
||||
kanbanDir: context.dirs.kanban,
|
||||
prefix: config.prefix,
|
||||
fs: context.fs,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:dew_core/dew_core.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
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 FileSystem fs;
|
||||
|
||||
/// Provides the current time for ticket creation. Injectable for testing.
|
||||
final DateTime Function() clock;
|
||||
|
||||
const TicketStore({
|
||||
required this.kanbanDir,
|
||||
required this.prefix,
|
||||
this.fs = const LocalFileSystem(),
|
||||
this.clock = DateTime.now,
|
||||
});
|
||||
|
||||
Future<Ticket> create({
|
||||
|
|
@ -31,7 +35,7 @@ class TicketStore {
|
|||
title: title,
|
||||
type: type,
|
||||
column: column,
|
||||
created: DateTime.now().toUtc(),
|
||||
created: clock().toUtc(),
|
||||
body: body,
|
||||
comments: const [],
|
||||
milestones: milestones,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue