diff --git a/src/Nagger.Data/Fake/FakeRemoteTaskRepository.cs b/src/Nagger.Data/Fake/FakeRemoteTaskRepository.cs index b1e1d30..a0abfd5 100644 --- a/src/Nagger.Data/Fake/FakeRemoteTaskRepository.cs +++ b/src/Nagger.Data/Fake/FakeRemoteTaskRepository.cs @@ -49,6 +49,11 @@ public IEnumerable GetTasksByProjectId(string projectId, string lastTaskId throw new NotImplementedException(); } + public void InitializeForProject(Project project) + { + throw new NotImplementedException(); + } + static Task GetTask(string id, string name, Task parent = null) { return new Task diff --git a/src/Nagger.Data/JIRA/BaseJiraRepository.cs b/src/Nagger.Data/JIRA/BaseJiraRepository.cs index 12925e0..61ad601 100644 --- a/src/Nagger.Data/JIRA/BaseJiraRepository.cs +++ b/src/Nagger.Data/JIRA/BaseJiraRepository.cs @@ -1,5 +1,6 @@ namespace Nagger.Data.JIRA { + using Extensions; using Interfaces; using Models; @@ -9,6 +10,7 @@ public class BaseJiraRepository const string UsernameKey = "JiraUsername"; const string PasswordKey = "JiraPassword"; const string ApiBaseUrlKey = "JiraApi"; + const string ModifiedFormat = "{0}_{1}"; readonly BaseRepository _baseRepository; User _user; @@ -23,7 +25,8 @@ public string ApiBaseUrl { get { - _apiBaseUrl = _apiBaseUrl ?? _baseRepository.GetApiBaseUrl(ApiName, ApiBaseUrlKey); + var apiKey = GetSettingKey(ApiBaseUrlKey); + _apiBaseUrl = _apiBaseUrl ?? _baseRepository.GetApiBaseUrl(ApiName, apiKey); return _apiBaseUrl; } } @@ -32,11 +35,20 @@ public User JiraUser { get { - _user = _user ?? _baseRepository.GetUser(ApiName, UsernameKey, PasswordKey); + var uKey = GetSettingKey(UsernameKey); + var pKey = GetSettingKey(PasswordKey); + _user = _user ?? _baseRepository.GetUser(ApiName, uKey, pKey); return _user; } } + public string GetSettingKey(string key) + { + return !KeyModifier.IsNullOrWhitespace() ? ModifiedFormat.FormatWith(KeyModifier, key) : key; + } + + public string KeyModifier { get; set; } + public bool UserExists => JiraUser != null; } } diff --git a/src/Nagger.Data/JIRA/JiraTaskRepository.cs b/src/Nagger.Data/JIRA/JiraTaskRepository.cs index 2557f10..1c94c7a 100644 --- a/src/Nagger.Data/JIRA/JiraTaskRepository.cs +++ b/src/Nagger.Data/JIRA/JiraTaskRepository.cs @@ -11,13 +11,21 @@ public class JiraTaskRepository : IRemoteTaskRepository { - readonly JiraApi _api; + JiraApi _api; // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable readonly BaseJiraRepository _baseJiraRepository; + JiraApi Api => _api ?? (_api = new JiraApi(_baseJiraRepository.JiraUser, _baseJiraRepository.ApiBaseUrl)); + + public JiraTaskRepository(BaseJiraRepository baseJiraRepository) { _baseJiraRepository = baseJiraRepository; + } + + public void InitializeForProject(Project project) + { + _baseJiraRepository.KeyModifier = project.Id; _api = new JiraApi(_baseJiraRepository.JiraUser, _baseJiraRepository.ApiBaseUrl); } @@ -71,7 +79,7 @@ public IEnumerable GetTasks(string lastTaskId = null) }); } - var apiResult = _api.Execute(request); + var apiResult = Api.Execute(request); return apiResult?.issues?.Select(x => new Task { @@ -102,7 +110,7 @@ public Task GetTaskByName(string name) } }; - var apiResult = _api.Execute(request); + var apiResult = Api.Execute(request); if (apiResult?.issues == null || !apiResult.issues.Any()) return null; @@ -148,7 +156,7 @@ public IEnumerable GetTasks(Project project) } }; - var apiResult = _api.Execute(request); + var apiResult = Api.Execute(request); return apiResult?.issues?.Select(x => new Task { @@ -203,7 +211,7 @@ public IEnumerable GetTasksByProjectId(string projectId, string lastTaskId } }; - var apiResult = _api.Execute(request); + var apiResult = Api.Execute(request); if (apiResult?.issues == null) yield break; foreach (var issue in apiResult.issues) diff --git a/src/Nagger.Data/JIRA/JiraTimeRepository.cs b/src/Nagger.Data/JIRA/JiraTimeRepository.cs index a84a98d..b3f3dbc 100644 --- a/src/Nagger.Data/JIRA/JiraTimeRepository.cs +++ b/src/Nagger.Data/JIRA/JiraTimeRepository.cs @@ -1,28 +1,75 @@ namespace Nagger.Data.JIRA { + using System; using API; using DTO; using Extensions; using Interfaces; using Models; using RestSharp; + using Project = Models.Project; - public class JiraTimeRepository : IRemoteTimeRepository + public class JiraTimeRepository : IRemoteTimeRepository, IInitializable { - readonly JiraApi _api; - + JiraApi _api; + readonly BaseJiraRepository _baseJiraRepository; + readonly ISettingsService _settingsService; + readonly IInputService _inputService; + const string AdjustEstimateKey = "AdjustJiraEstimate"; + JiraApi Api => _api ?? (_api = new JiraApi(_baseJiraRepository.JiraUser, _baseJiraRepository.ApiBaseUrl)); - public JiraTimeRepository(BaseJiraRepository baseJiraRepository) + public JiraTimeRepository(BaseJiraRepository baseJiraRepository, ISettingsService settingsService, IInputService inputService) { - _api = new JiraApi(baseJiraRepository.JiraUser, baseJiraRepository.ApiBaseUrl); + _baseJiraRepository = baseJiraRepository; + _settingsService = settingsService; + _inputService = inputService; } - // needs to post to: /rest/api/2/issue/{issueIdOrKey}/worklog public bool RecordTime(TimeEntry timeEntry) { - // Jira requires a task if (!timeEntry.HasTask) return false; - + return RecordTime(timeEntry, timeEntry.Task); + } + + public bool RecordAssociatedTime(TimeEntry timeEntry) + { + if (!timeEntry.HasAssociatedTask) return false; + try + { + return RecordTime(timeEntry, timeEntry.AssociatedTask); + } + catch (ApplicationException) + { + return false; + } + } + + public void InitializeForProject(Project project) + { + _baseJiraRepository.KeyModifier = project.Id; + _api = new JiraApi(_baseJiraRepository.JiraUser, _baseJiraRepository.ApiBaseUrl); + AdjustJiraEstimate(); + } + + public void Initialize() + { + AdjustJiraEstimate(); + } + + bool AdjustJiraEstimate(Project project = null) + { + var settingKey = _baseJiraRepository.GetSettingKey(AdjustEstimateKey); + if (!_settingsService.GetSetting(settingKey).IsNullOrWhitespace()) return _settingsService.GetSetting(settingKey); + + var projectString = (project != null) ? " for {0} ".FormatWith(project.Name) : ""; + var adjustJiraEstimate = _inputService.AskForBoolean("Would you like JIRA to automatically adjust estimates {0}when you log your time?".FormatWith(projectString)); + _settingsService.SaveSetting(settingKey, adjustJiraEstimate.ToString()); + return adjustJiraEstimate; + } + + // needs to post to: /rest/api/2/issue/{issueIdOrKey}/worklog + bool RecordTime(TimeEntry timeEntry, Task task) + { // jira requires a special format - like ISO 8601 but not quite const string jiraTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffK"; @@ -36,16 +83,18 @@ public bool RecordTime(TimeEntry timeEntry) timeSpent = timeEntry.MinutesSpent +"m" }; + var adjustEstimate = !AdjustJiraEstimate() ? "?adjustEstimate=leave": ""; + var post = new RestRequest() { - Resource = "issue/"+timeEntry.Task.Id+"/worklog?adjustEstimate=leave", + Resource = "issue/{0}/worklog{1}".FormatWith(task.Id, adjustEstimate), Method = Method.POST, RequestFormat = DataFormat.Json }; post.AddBody(worklog); - var result = _api.Execute(post); + var result = Api.Execute(post); return result != null; } } diff --git a/src/Nagger.Data/Local/LocalProjectRepository.cs b/src/Nagger.Data/Local/LocalProjectRepository.cs index fc71fcd..4d23052 100644 --- a/src/Nagger.Data/Local/LocalProjectRepository.cs +++ b/src/Nagger.Data/Local/LocalProjectRepository.cs @@ -1,6 +1,9 @@ namespace Nagger.Data.Local { + using System; using System.Collections.Generic; + using System.Linq; + using Extensions; using Interfaces; using Models; @@ -12,7 +15,7 @@ public Project GetProjectById(string id) using (var cmd = cnn.CreateCommand()) { cmd.CommandText = @"SELECT * FROM Projects - WHERE Id = @id"; + WHERE Projects.Id = @id"; cmd.Prepare(); cmd.Parameters.AddWithValue("@id", id); @@ -28,6 +31,8 @@ public Project GetProjectById(string id) Key = reader.Get("Key") }; + project.AssociatedRemoteRepository = GetAssociatedRemoteRepository(project.Id); + return project; } } @@ -52,9 +57,11 @@ public Project GetProjectByKey(string key) { Id = reader.Get("Id"), Name = reader.Get("Name"), - Key = reader.Get("Key") + Key = reader.Get("Key"), }; + project.AssociatedRemoteRepository = GetAssociatedRemoteRepository(project.Id); + return project; } } @@ -77,9 +84,11 @@ public IEnumerable GetProjects() { Id = reader.Get("Id"), Name = reader.Get("Name"), - Key = reader.Get("Key") + Key = reader.Get("Key"), }; + project.AssociatedRemoteRepository = GetAssociatedRemoteRepository(project.Id); + projects.Add(project); } } @@ -93,8 +102,7 @@ public void StoreProject(Project project) using (var cnn = GetConnection()) using (var cmd = cnn.CreateCommand()) { - cmd.CommandText = @"INSERT OR IGNORE INTO Projects (Id, Name, Key) - VALUES (@Id, @Name, @Key)"; + cmd.CommandText = @"INSERT OR IGNORE INTO Projects (Id, Name, Key) VALUES (@Id, @Name, @Key)"; cmd.Prepare(); cmd.Parameters.AddWithValue("@Id", project.Id); @@ -103,6 +111,24 @@ public void StoreProject(Project project) cmd.ExecuteNonQuery(); } + + StoreAssociatedRepository(project.Id, project.AssociatedRemoteRepository.ToString()); + + } + + void StoreAssociatedRepository(string projectId, string remoteRepository) + { + using (var cnn = GetConnection()) + using (var cmd = cnn.CreateCommand()) + { + cmd.CommandText = @"INSERT OR REPLACE INTO AssociatedRemoteRepositories (ProjectId, RemoteRepository) VALUES (@ProjectId, @RemoteRepository)"; + + cmd.Prepare(); + cmd.Parameters.AddWithValue("@ProjectId", projectId); + cmd.Parameters.AddWithValue("@RemoteRepository", remoteRepository); + + cmd.ExecuteNonQuery(); + } } public Project GetProjectByName(string name) @@ -126,10 +152,42 @@ public Project GetProjectByName(string name) Name = reader.Get("Name"), Key = reader.Get("Key") }; + project.AssociatedRemoteRepository = GetAssociatedRemoteRepository(project.Id); return project; } } } + + SupportedRemoteRepository? GetAssociatedRemoteRepository(string projectId) + { + var repositories = GetAssociatedRemoteRepositories(projectId).ToList(); + if (!repositories.Any()) return null; + return repositories.First(); + } + + IEnumerable GetAssociatedRemoteRepositories(string projectId) + { + using (var cnn = GetConnection()) + using (var cmd = cnn.CreateCommand()) + { + cmd.CommandText = @"SELECT RemoteRepository FROM AssociatedRemoteRepositories + WHERE ProjectId = @ProjectId"; + + cmd.Prepare(); + cmd.Parameters.AddWithValue("@ProjectId", projectId); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var repositoryString = reader.Get("RemoteRepository"); + if (repositoryString.IsNullOrWhitespace()) continue; + SupportedRemoteRepository remoteRepository; + if (Enum.TryParse(repositoryString, out remoteRepository)) yield return remoteRepository; + } + } + } + } } } \ No newline at end of file diff --git a/src/Nagger.Data/Local/LocalTimeRepository.cs b/src/Nagger.Data/Local/LocalTimeRepository.cs index 929c17a..0bb2d47 100644 --- a/src/Nagger.Data/Local/LocalTimeRepository.cs +++ b/src/Nagger.Data/Local/LocalTimeRepository.cs @@ -24,8 +24,8 @@ public void RecordTime(TimeEntry timeEntry) using (var cmd = cnn.CreateCommand()) { cmd.CommandText = - @"INSERT INTO TimeEntries (TimeRecorded, Comment, MinutesSpent, TaskId, ProjectId, Internal) - VALUES (@TimeRecorded, @Comment, @MinutesSpent, @TaskId, @ProjectId, @Internal)"; + @"INSERT INTO TimeEntries (TimeRecorded, Comment, MinutesSpent, TaskId, ProjectId, Internal, AssociatedTaskId) + VALUES (@TimeRecorded, @Comment, @MinutesSpent, @TaskId, @ProjectId, @Internal, @AssociatedTaskId)"; cmd.Prepare(); cmd.Parameters.AddWithValue("@TimeRecorded", timeEntry.TimeRecorded); @@ -34,6 +34,7 @@ public void RecordTime(TimeEntry timeEntry) cmd.Parameters.AddWithValue("@Internal", timeEntry.Internal); cmd.Parameters.AddWithValue("@TaskId", (timeEntry.Task == null) ? "" : timeEntry.Task.Id); + cmd.Parameters.AddWithValue("@AssociatedTaskId", timeEntry.AssociatedTask == null ? "" : timeEntry.AssociatedTask.Id); cmd.Parameters.AddWithValue("@ProjectId", (timeEntry.Project == null) ? "" : timeEntry.Project.Id); cmd.ExecuteNonQuery(); @@ -47,7 +48,7 @@ public TimeEntry GetLastTimeEntry(bool getInternal = false) { var internalWhere = (getInternal) ? "" : "WHERE Internal = 0"; - cmd.CommandText = "SELECT * FROM TimeEntries " + internalWhere + " ORDER BY Id DESC LIMIT 1"; + cmd.CommandText = "SELECT * FROM TimeEntries "+internalWhere+" ORDER BY Id DESC LIMIT 1"; using (var reader = cmd.ExecuteReader()) { @@ -61,6 +62,7 @@ public TimeEntry GetLastTimeEntry(bool getInternal = false) Comment = reader.Get("Comment"), Id = reader.Get("Id"), Task = _localTaskRepository.GetTaskById(reader.Get("TaskId")), + AssociatedTask = _localTaskRepository.GetTaskById(reader.Get("AssociatedTaskId")), Synced = reader.Get("Synced"), MinutesSpent = reader.Get("MinutesSpent"), Internal = reader.Get("Internal"), @@ -94,6 +96,53 @@ public IEnumerable GetUnsyncedEntries(bool getInternal = false) Comment = reader.Get("Comment"), Id = reader.Get("Id"), Task = _localTaskRepository.GetTaskById(reader.Get("TaskId")), + AssociatedTask = _localTaskRepository.GetTaskById(reader.Get("AssociatedTaskId")), + Synced = reader.Get("Synced"), + MinutesSpent = reader.Get("MinutesSpent"), + Internal = reader.Get("Internal"), + Project = _localProjectRepository.GetProjectById(reader.Get("ProjectId")) + }; + + entries.Add(timeEntry); + } + } + } + return entries; + } + + public IEnumerable GetTimeEntries(IList entryIds, bool getInternal = false) + { + var entries = new List(); + using (var cnn = GetConnection()) + using (var cmd = cnn.CreateCommand()) + { + var paramArgs = new List(); + for (var i = 0; i < entryIds.Count; i++) + { + paramArgs.Add("@entryId"+i); + } + + var cmdText = "SELECT * FROM TimeEntries WHERE Id IN("+string.Join(",", paramArgs)+")"; + if (!getInternal) cmdText += " AND Internal = 0"; + cmd.CommandText = cmdText; + cmd.Prepare(); + for (var i = 0; i < entryIds.Count; i++) + { + cmd.Parameters.AddWithValue("@entryId" + i, entryIds[i]); + } + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var timeEntry = new TimeEntry + { + TimeRecorded = + DateTime.SpecifyKind(reader.GetDateTime(reader.GetOrdinal("TimeRecorded")), + DateTimeKind.Local), + Comment = reader.Get("Comment"), + Id = reader.Get("Id"), + Task = _localTaskRepository.GetTaskById(reader.Get("TaskId")), + AssociatedTask = _localTaskRepository.GetTaskById(reader.Get("AssociatedTaskId")), Synced = reader.Get("Synced"), MinutesSpent = reader.Get("MinutesSpent"), Internal = reader.Get("Internal"), @@ -200,6 +249,31 @@ ORDER BY TimeRecorded DESC return comments; } + public IEnumerable GetRecentlyAssociatedTaskIds(int limit, string projectId) + { + var associatedTaskIds = new List(); + using (var cnn = GetConnection()) + using (var cmd = cnn.CreateCommand()) + { + cmd.CommandText = @"SELECT AssociatedTaskId + FROM TimeEntries + WHERE Internal = 0 AND ProjectId = @projectId AND AssociatedTaskId != '' + GROUP BY AssociatedTaskId + ORDER BY TimeRecorded DESC + LIMIT @limit"; + cmd.Parameters.AddWithValue("@projectId", projectId); + cmd.Parameters.AddWithValue("@limit", limit); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + associatedTaskIds.Add(reader.Get("AssociatedTaskId")); + } + } + } + return associatedTaskIds; + } + public void RemoveTimeEntries(IEnumerable entries) { if (!entries.Any()) return; diff --git a/src/Nagger.Data/Meazure/MeazureTaskRepository.cs b/src/Nagger.Data/Meazure/MeazureTaskRepository.cs index 980e194..c9dc794 100644 --- a/src/Nagger.Data/Meazure/MeazureTaskRepository.cs +++ b/src/Nagger.Data/Meazure/MeazureTaskRepository.cs @@ -55,5 +55,9 @@ public IEnumerable GetTasksByProjectId(string projectId, string lastTaskId { return GetTasks(); } + + public void InitializeForProject(Project project) + { + } } } diff --git a/src/Nagger.Data/Meazure/MeazureTimeRepository.cs b/src/Nagger.Data/Meazure/MeazureTimeRepository.cs index b86ac9e..a13d921 100644 --- a/src/Nagger.Data/Meazure/MeazureTimeRepository.cs +++ b/src/Nagger.Data/Meazure/MeazureTimeRepository.cs @@ -6,6 +6,7 @@ using Interfaces; using Models; using RestSharp; + using Project = Models.Project; public class MeazureTimeRepository : IRemoteTimeRepository { @@ -19,8 +20,18 @@ public MeazureTimeRepository(ISettingsService settingsService, IInputService inp public bool RecordTime(TimeEntry timeEntry) { - // meazure requires either Notes, a project, or a task if (!timeEntry.HasProject && !timeEntry.HasTask && !timeEntry.HasComment) return false; + return RecordTime(timeEntry, timeEntry.Task); + } + + public bool RecordAssociatedTime(TimeEntry timeEntry) + { + if (!timeEntry.HasProject && !timeEntry.HasTask && !timeEntry.HasComment) return false; + return RecordTime(timeEntry, timeEntry.AssociatedTask); + } + + bool RecordTime(TimeEntry timeEntry, Task task) + { var timeEntryModel = new TimeEntryModel { @@ -29,7 +40,7 @@ public bool RecordTime(TimeEntry timeEntry) TimeString = timeEntry.MinutesSpent + "m", DurationSeconds = timeEntry.MinutesSpent*60, ProjectId = timeEntry.Project?.Id, - TaskId = timeEntry.Task?.Id, + TaskId = task?.Id, WorkItems = new List(), //TODO: add functionality for tracking WorkItems }; @@ -45,5 +56,9 @@ public bool RecordTime(TimeEntry timeEntry) var result = _api.Execute(post); return result != null; } + + public void InitializeForProject(Project project) + { + } } } diff --git a/src/Nagger.Data/nagger.ndb b/src/Nagger.Data/nagger.ndb index 1d7e612..891bccc 100644 Binary files a/src/Nagger.Data/nagger.ndb and b/src/Nagger.Data/nagger.ndb differ diff --git a/src/Nagger.Extensions/StringExtensions.cs b/src/Nagger.Extensions/StringExtensions.cs index 7260516..65496c5 100644 --- a/src/Nagger.Extensions/StringExtensions.cs +++ b/src/Nagger.Extensions/StringExtensions.cs @@ -35,5 +35,10 @@ public static bool IsNullOrWhitespace(this string source) { return string.IsNullOrWhiteSpace(source); } + + public static string FormatWith(this string source, params object[] args) + { + return string.Format(source, args); + } } } \ No newline at end of file diff --git a/src/Nagger.Interfaces/IAssociatedRemoteRepositoryService.cs b/src/Nagger.Interfaces/IAssociatedRemoteRepositoryService.cs new file mode 100644 index 0000000..7e84c22 --- /dev/null +++ b/src/Nagger.Interfaces/IAssociatedRemoteRepositoryService.cs @@ -0,0 +1,11 @@ +namespace Nagger.Interfaces +{ + using Models; + + public interface IAssociatedRemoteRepositoryService + { + IRemoteTaskRepository GetAssociatedRemoteTaskRepository(Project project); + IRemoteTimeRepository GetAssociatedRemoteTimeRepository(Project project); + void InitializeAssociatedRepositories(Project project); + } +} diff --git a/src/Nagger.Interfaces/IInitializable.cs b/src/Nagger.Interfaces/IInitializable.cs new file mode 100644 index 0000000..48927be --- /dev/null +++ b/src/Nagger.Interfaces/IInitializable.cs @@ -0,0 +1,7 @@ +namespace Nagger.Interfaces +{ + public interface IInitializable + { + void Initialize(); + } +} diff --git a/src/Nagger.Interfaces/ILocalTimeRepository.cs b/src/Nagger.Interfaces/ILocalTimeRepository.cs index a2045b7..29653cc 100644 --- a/src/Nagger.Interfaces/ILocalTimeRepository.cs +++ b/src/Nagger.Interfaces/ILocalTimeRepository.cs @@ -14,8 +14,10 @@ public interface ILocalTimeRepository TimeEntry GetLastTimeEntry(bool getInternal = false); IEnumerable GetUnsyncedEntries(bool getInternal = false); + IEnumerable GetTimeEntries(IList entryIds, bool getInternal = false); IEnumerable GetTimeEntriesSince(DateTime time, bool getInternal = false); IEnumerable GetRecentlyRecordedTaskIds(int limit); IEnumerable GetRecentlyRecordedCommentsForTaskId(int limit, string taskId); + IEnumerable GetRecentlyAssociatedTaskIds(int limit, string projectId); } -} \ No newline at end of file +} diff --git a/src/Nagger.Interfaces/IProjectService.cs b/src/Nagger.Interfaces/IProjectService.cs index 9df482c..77a5aac 100644 --- a/src/Nagger.Interfaces/IProjectService.cs +++ b/src/Nagger.Interfaces/IProjectService.cs @@ -10,5 +10,7 @@ public interface IProjectService Project GetProjectById(string id); Project GetProjectByKey(string key); Project GetProjectByName(string name); + + void AssociateProjectWithRepository(Project project, SupportedRemoteRepository repository); } } diff --git a/src/Nagger.Interfaces/IRemoteRunner.cs b/src/Nagger.Interfaces/IRemoteRunner.cs index e4237f9..e8d4af8 100644 --- a/src/Nagger.Interfaces/IRemoteRunner.cs +++ b/src/Nagger.Interfaces/IRemoteRunner.cs @@ -5,5 +5,7 @@ public interface IRemoteRunner { Task AskForTask(); + + Task AskForAssociatedTask(Task currentTask); } } diff --git a/src/Nagger.Interfaces/IRemoteTaskRepository.cs b/src/Nagger.Interfaces/IRemoteTaskRepository.cs index abba85a..77623dc 100644 --- a/src/Nagger.Interfaces/IRemoteTaskRepository.cs +++ b/src/Nagger.Interfaces/IRemoteTaskRepository.cs @@ -9,5 +9,7 @@ public interface IRemoteTaskRepository IEnumerable GetTasks(); IEnumerable GetTasks(Project project); IEnumerable GetTasksByProjectId(string projectId, string lastTaskId = null); + + void InitializeForProject(Project project); } } diff --git a/src/Nagger.Interfaces/IRemoteTimeRepository.cs b/src/Nagger.Interfaces/IRemoteTimeRepository.cs index 1c74192..e88263f 100644 --- a/src/Nagger.Interfaces/IRemoteTimeRepository.cs +++ b/src/Nagger.Interfaces/IRemoteTimeRepository.cs @@ -5,5 +5,7 @@ public interface IRemoteTimeRepository { bool RecordTime(TimeEntry timeEntry); + bool RecordAssociatedTime(TimeEntry timeEntry); + void InitializeForProject(Project project); } } diff --git a/src/Nagger.Interfaces/ITaskService.cs b/src/Nagger.Interfaces/ITaskService.cs index af5c6bb..c02de2f 100644 --- a/src/Nagger.Interfaces/ITaskService.cs +++ b/src/Nagger.Interfaces/ITaskService.cs @@ -7,6 +7,7 @@ public interface ITaskService { Task GetLastTask(); Task GetTaskByName(string name); + Task GetAssociatedTaskByName(string name, Project project); Task GetTaskById(string taskId); void StoreTask(Task task); IEnumerable GetGeneralTasks(); diff --git a/src/Nagger.Interfaces/ITimeService.cs b/src/Nagger.Interfaces/ITimeService.cs index 6464c1f..1cf65e3 100644 --- a/src/Nagger.Interfaces/ITimeService.cs +++ b/src/Nagger.Interfaces/ITimeService.cs @@ -8,8 +8,8 @@ public interface ITimeService { void RecordTime(Task task); void RecordTime(TimeEntry timeEntry); - void RecordTime(Task task, DateTime time, string comment); - void RecordTime(Task task, int intervalCount, int minutesWorked, DateTime offset, string comment); + void RecordTime(Task task, DateTime time, string comment, Task associatedTask); + void RecordTime(Task task, int intervalCount, int minutesWorked, DateTime offset, string comment, Task associatedTask); void RecordMarker(DateTime time); void DailyTimeOperations(bool force = false); void SquashTime(); // this will probably end up being internal to the time service @@ -19,6 +19,7 @@ public interface ITimeService int IntervalsSinceTime(DateTime startTime); int IntervalsSinceLastRecord(bool justToday = true); IEnumerable GetRecentlyRecordedTaskIds(int limit); + IEnumerable GetRecentlyAssociatedTaskIds(int limit, Task task); IEnumerable GetRecentlyRecordedCommentsForTask(int limit, Task task); string GetTimeReport(); diff --git a/src/Nagger.Interfaces/Nagger.Interfaces.csproj b/src/Nagger.Interfaces/Nagger.Interfaces.csproj index cbde99b..993fa5b 100644 --- a/src/Nagger.Interfaces/Nagger.Interfaces.csproj +++ b/src/Nagger.Interfaces/Nagger.Interfaces.csproj @@ -36,7 +36,9 @@ + + diff --git a/src/Nagger.Models/Nagger.Models.csproj b/src/Nagger.Models/Nagger.Models.csproj index a07f1cc..2651fc8 100644 --- a/src/Nagger.Models/Nagger.Models.csproj +++ b/src/Nagger.Models/Nagger.Models.csproj @@ -36,6 +36,7 @@ + diff --git a/src/Nagger.Models/Project.cs b/src/Nagger.Models/Project.cs index 6e48318..7205a0d 100644 --- a/src/Nagger.Models/Project.cs +++ b/src/Nagger.Models/Project.cs @@ -5,6 +5,7 @@ public class Project public string Id { get; set; } public string Name { get; set; } public string Key { get; set; } + public SupportedRemoteRepository? AssociatedRemoteRepository { get; set; } public override string ToString() { diff --git a/src/Nagger.Models/SupportedRemoteRepository.cs b/src/Nagger.Models/SupportedRemoteRepository.cs new file mode 100644 index 0000000..d65f983 --- /dev/null +++ b/src/Nagger.Models/SupportedRemoteRepository.cs @@ -0,0 +1,8 @@ +namespace Nagger.Models +{ + public enum SupportedRemoteRepository + { + Jira, + Meazure + } +} diff --git a/src/Nagger.Models/TimeEntry.cs b/src/Nagger.Models/TimeEntry.cs index ba30799..1dbef99 100644 --- a/src/Nagger.Models/TimeEntry.cs +++ b/src/Nagger.Models/TimeEntry.cs @@ -16,35 +16,39 @@ public TimeEntry(Task task, string comment = null) Project = task.Project; } - public TimeEntry(Task task, DateTime time, string comment = null) + public TimeEntry(Task task, DateTime time, string comment = null, Task associatedTask = null) { Task = task; TimeRecorded = time; Comment = comment; Project = task.Project; + AssociatedTask = associatedTask; } - public TimeEntry(Project project, int minutesSpent, string comment = null) + public TimeEntry(Project project, int minutesSpent, string comment = null, Task associatedTask = null) { TimeRecorded = DateTime.Now; Project = project; MinutesSpent = minutesSpent; Comment = comment; + AssociatedTask = associatedTask; } - public TimeEntry(Task task, int minutesSpent, string comment = null) + public TimeEntry(Task task, int minutesSpent, string comment = null, Task associatedTask = null) { TimeRecorded = DateTime.Now; Task = task; Comment = comment; MinutesSpent = minutesSpent; Project = task.Project; + AssociatedTask = associatedTask; } public int Id { get; set; } public DateTime TimeRecorded { get; set; } public string Comment { get; set; } public Task Task { get; set; } + public Task AssociatedTask { get; set; } public int MinutesSpent { get; set; } public Project Project { get; set; } public bool Synced { get; set; } @@ -53,6 +57,7 @@ public TimeEntry(Task task, int minutesSpent, string comment = null) public bool HasComment => !string.IsNullOrWhiteSpace(Comment); public bool HasProject => Project != null; public bool HasTask => Task != null; + public bool HasAssociatedTask => AssociatedTask != null; // note: no need to add a "user" we are going to work under the assumption that there is only one user } diff --git a/src/Nagger.Services/AssociatedRemoteRepositoryService.cs b/src/Nagger.Services/AssociatedRemoteRepositoryService.cs new file mode 100644 index 0000000..0cb5957 --- /dev/null +++ b/src/Nagger.Services/AssociatedRemoteRepositoryService.cs @@ -0,0 +1,42 @@ +namespace Nagger.Services +{ + using Autofac.Features.Indexed; + using Interfaces; + using Models; + + public class AssociatedRemoteRepositoryService : IAssociatedRemoteRepositoryService + { + readonly IIndex _remoteTaskRepositories; + readonly IIndex _remoteTimeRepositories; + + public AssociatedRemoteRepositoryService(IIndex remoteTaskRepositories, IIndex remoteTimeRepositories) + { + _remoteTaskRepositories = remoteTaskRepositories; + _remoteTimeRepositories = remoteTimeRepositories; + } + + public IRemoteTaskRepository GetAssociatedRemoteTaskRepository(Project project) + { + if (project.AssociatedRemoteRepository == null) return null; + + var remoteRepository = _remoteTaskRepositories[project.AssociatedRemoteRepository.Value]; + remoteRepository.InitializeForProject(project); + return remoteRepository; + } + + public IRemoteTimeRepository GetAssociatedRemoteTimeRepository(Project project) + { + if (project.AssociatedRemoteRepository == null) return null; + + var remoteRepository = _remoteTimeRepositories[project.AssociatedRemoteRepository.Value]; + remoteRepository.InitializeForProject(project); + return remoteRepository; + } + + public void InitializeAssociatedRepositories(Project project) + { + GetAssociatedRemoteTaskRepository(project); + GetAssociatedRemoteTimeRepository(project); + } + } +} \ No newline at end of file diff --git a/src/Nagger.Services/JIRA/JiraRunner.cs b/src/Nagger.Services/JIRA/JiraRunner.cs index 7b8d816..a8de943 100644 --- a/src/Nagger.Services/JIRA/JiraRunner.cs +++ b/src/Nagger.Services/JIRA/JiraRunner.cs @@ -50,6 +50,12 @@ public Task AskForTask() return AskForSpecificTask(); } + public Task AskForAssociatedTask(Task currentTask) + { + // associated tasks are not supported for JIRA at the moment + return null; + } + static string OutputProjects(ICollection projects) { var sb = new StringBuilder(); diff --git a/src/Nagger.Services/Meazure/MeazureRunner.cs b/src/Nagger.Services/Meazure/MeazureRunner.cs index 3291fe8..31c3c6e 100644 --- a/src/Nagger.Services/Meazure/MeazureRunner.cs +++ b/src/Nagger.Services/Meazure/MeazureRunner.cs @@ -1,5 +1,7 @@ namespace Nagger.Services.Meazure { + using System; + using System.Collections.Generic; using System.Linq; using Interfaces; using Models; @@ -23,7 +25,7 @@ public Task AskForTask() { var mostRecentTask = _taskService.GetLastTask(); - var currentProject = AskAboutProject(mostRecentTask); + var currentProject = GetProject(mostRecentTask); var recentTaskNames = _taskService.GetTasksByTaskIds(_timeService.GetRecentlyRecordedTaskIds(5)).Select(x=>x.Name); var taskName = _inputService.AskForSelectionOrInput("Choose from a recent task. (If none of these match what you are doing then just leave blank.)", recentTaskNames.ToList()); @@ -42,6 +44,40 @@ public Task AskForTask() return currentTask; } + public Task AskForAssociatedTask(Task currentTask) + { + if (!_inputService.AskForBoolean("Associate this entry with an additional task?")) return null; + + Task associatedTask; + var recentlyAssociatedTasks = _taskService.GetTasksByTaskIds(_timeService.GetRecentlyAssociatedTaskIds(5, currentTask)).ToDictionary(key=> key.Name); + var associatedTaskName = _inputService.AskForSelectionOrInput("Choose from a recent task or insert a new task name", + recentlyAssociatedTasks.Keys.ToList()); + + if (recentlyAssociatedTasks.TryGetValue(associatedTaskName, out associatedTask)) return associatedTask; + + return _taskService.GetAssociatedTaskByName(associatedTaskName, currentTask.Project); + } + + //TODO: Pull this function and the one in Program.cs out into a shared location + static IEnumerable SupportedRemoteRepositories() + { + return Enum.GetValues(typeof(SupportedRemoteRepository)).Cast(); + } + + Project GetProject(Task currentTask) + { + var project = AskAboutProject(currentTask); + + if (!project.AssociatedRemoteRepository.HasValue && _inputService.AskForBoolean("Would you like to record time for this project in an additional repository?")) + { + var selectedRepository = _inputService.AskForSelection("What will your additional remote repository be?", + SupportedRemoteRepositories().ToList()); + _projectService.AssociateProjectWithRepository(project, selectedRepository); + } + + return project; + } + Project AskAboutProject(Task currentTask) { if (currentTask?.Project != null && _inputService.AskForBoolean($"Are you still working on {currentTask.Project.Name}?")) diff --git a/src/Nagger.Services/Nagger.Services.csproj b/src/Nagger.Services/Nagger.Services.csproj index 13a7e65..6127ded 100644 --- a/src/Nagger.Services/Nagger.Services.csproj +++ b/src/Nagger.Services/Nagger.Services.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Nagger.Services/NaggerRunner.cs b/src/Nagger.Services/NaggerRunner.cs index 982bda4..2274847 100644 --- a/src/Nagger.Services/NaggerRunner.cs +++ b/src/Nagger.Services/NaggerRunner.cs @@ -38,6 +38,7 @@ public void Run() var lastTimeEntry = _timeService.GetLastTimeEntry(); var currentTask = lastTimeEntry?.Task; var comment = ""; + var associatedTask = (Task)null; if (currentTask != null) { @@ -48,7 +49,11 @@ public void Run() stillWorking = _inputService.AskForBoolean($"Are you still working on {lastTimeEntry.Comment} ({currentTask.Name})?"); - if (stillWorking) comment = lastTimeEntry.Comment; + if (stillWorking) + { + comment = lastTimeEntry.Comment; + associatedTask = lastTimeEntry.AssociatedTask; + } } if(!stillWorking) @@ -96,27 +101,32 @@ public void Run() comment = _inputService.AskForSelectionOrInput("Choose from options or insert a new comment. (Leave Blank for no comment)", recentComments); } + if (associatedTask == null) + { + associatedTask = _remoteRunner.AskForAssociatedTask(currentTask); + } + //todo: refactor the way runMiss is done runMiss = _timeService.IntervalsSinceLastRecord(); // there will usually be 1 interval between the last time this ran and this time (it only makes sense) - if (runMiss <= 1) _timeService.RecordTime(currentTask, askTime, comment); + if (runMiss <= 1) _timeService.RecordTime(currentTask, askTime, comment, associatedTask); else { - AskAboutBreak(currentTask, askTime, runMiss, comment); + AskAboutBreak(currentTask, askTime, runMiss, comment, associatedTask); } _outputService.HideInterface(); } - void AskAboutBreak(Task currentTask, DateTime askTime, int missedInterval, string comment) + void AskAboutBreak(Task currentTask, DateTime askTime, int missedInterval, string comment, Task associatedTask) { if ( !_inputService.AskForBoolean("Looks like we missed " + missedInterval + " check in(s). Were you on break?")) { - _timeService.RecordTime(currentTask, askTime, comment); + _timeService.RecordTime(currentTask, askTime, comment, associatedTask); // record an entry for now in the case where they forgot about nagger and are just now answering the questions - if(_timeService.IntervalsSinceTime(askTime) > 1) _timeService.RecordTime(currentTask, DateTime.Now, comment); + if(_timeService.IntervalsSinceTime(askTime) > 1) _timeService.RecordTime(currentTask, DateTime.Now, comment, associatedTask); //TODO: Create a method to ask about abscences (what if they have worked on multiple things in the amount of time they were gone?) } @@ -138,11 +148,11 @@ void AskAboutBreak(Task currentTask, DateTime askTime, int missedInterval, strin "Which of these options represents about how long you have been working?", intervalsMissed); // insert an entry for when they started working - _timeService.RecordTime(currentTask, missedInterval, minutesWorked, lastTime, comment); + _timeService.RecordTime(currentTask, missedInterval, minutesWorked, lastTime, comment, associatedTask); } // also insert an entry for the current time (since they are working and are no longer on break) - _timeService.RecordTime(currentTask, DateTime.Now, comment); + _timeService.RecordTime(currentTask, DateTime.Now, comment, associatedTask); } } } diff --git a/src/Nagger.Services/ProjectService.cs b/src/Nagger.Services/ProjectService.cs index 7ceba2e..f8db947 100644 --- a/src/Nagger.Services/ProjectService.cs +++ b/src/Nagger.Services/ProjectService.cs @@ -8,12 +8,14 @@ public class ProjectService : IProjectService { readonly ILocalProjectRepository _localProjectRepository; readonly IRemoteProjectRepository _remoteProjectRepository; + readonly IAssociatedRemoteRepositoryService _associatedRemoteRepositoryService; public ProjectService(ILocalProjectRepository localProjectRepository, - IRemoteProjectRepository remoteProjectRepository) + IRemoteProjectRepository remoteProjectRepository, IAssociatedRemoteRepositoryService associatedRemoteRepositoryService) { _localProjectRepository = localProjectRepository; _remoteProjectRepository = remoteProjectRepository; + _associatedRemoteRepositoryService = associatedRemoteRepositoryService; } public IEnumerable GetProjects() @@ -37,6 +39,13 @@ public Project GetProjectByName(string name) return _localProjectRepository.GetProjectByName(name); } + public void AssociateProjectWithRepository(Project project, SupportedRemoteRepository repository) + { + project.AssociatedRemoteRepository = repository; + _associatedRemoteRepositoryService.InitializeAssociatedRepositories(project); + _localProjectRepository.StoreProject(project); + } + void SyncProjectsWithRemote() { /** diff --git a/src/Nagger.Services/TaskService.cs b/src/Nagger.Services/TaskService.cs index cade640..25643e8 100644 --- a/src/Nagger.Services/TaskService.cs +++ b/src/Nagger.Services/TaskService.cs @@ -9,11 +9,13 @@ public class TaskService : ITaskService { readonly ILocalTaskRepository _localTaskRepository; readonly IRemoteTaskRepository _remoteTaskRepository; + readonly IAssociatedRemoteRepositoryService _associatedRemoteRepositoryService; - public TaskService(ILocalTaskRepository localTaskRepository, IRemoteTaskRepository remoteTaskRepository) + public TaskService(ILocalTaskRepository localTaskRepository, IRemoteTaskRepository remoteTaskRepository, IAssociatedRemoteRepositoryService associatedRemoteRepositoryService) { _localTaskRepository = localTaskRepository; _remoteTaskRepository = remoteTaskRepository; + _associatedRemoteRepositoryService = associatedRemoteRepositoryService; } public Task GetLastTask() @@ -31,6 +33,20 @@ public Task GetTaskByName(string name) return task; } + public Task GetAssociatedTaskByName(string name, Project project) + { + var task = _localTaskRepository.GetTaskByName(name); + if (task != null) return task; + + var remoteTaskRepository = _associatedRemoteRepositoryService.GetAssociatedRemoteTaskRepository(project); + if (remoteTaskRepository == null) return null; + + //TODO: we need to make sure the id stored is uniqe. We could do this by specifying that this is the remoteId and then internally tracking via auto-incremeneting key. + task = remoteTaskRepository.GetTaskByName(name); + if (task != null) StoreTask(task); + return task; + } + public Task GetTaskById(string taskId) { return _localTaskRepository.GetTaskById(taskId); diff --git a/src/Nagger.Services/TimeService.cs b/src/Nagger.Services/TimeService.cs index c07fe54..ae18901 100644 --- a/src/Nagger.Services/TimeService.cs +++ b/src/Nagger.Services/TimeService.cs @@ -11,17 +11,17 @@ public class TimeService : ITimeService { readonly ILocalTimeRepository _localTimeRepository; - readonly IProjectService _projectService; readonly IRemoteTimeRepository _remoteTimeRepository; readonly ISettingsService _settingsService; + readonly IAssociatedRemoteRepositoryService _associatedRemoteRepositoryService; public TimeService(ILocalTimeRepository localTimeRepository, IRemoteTimeRepository remoteTimeRepository, - ISettingsService settingsService, IProjectService projectService) + ISettingsService settingsService, IAssociatedRemoteRepositoryService associatedRemoteRepositoryService) { _localTimeRepository = localTimeRepository; _remoteTimeRepository = remoteTimeRepository; _settingsService = settingsService; - _projectService = projectService; + _associatedRemoteRepositoryService = associatedRemoteRepositoryService; } int NaggingInterval @@ -35,18 +35,18 @@ public void RecordTime(Task task) _localTimeRepository.RecordTime(timeEntry); } - public void RecordTime(Task task, DateTime time, string comment) + public void RecordTime(Task task, DateTime time, string comment, Task associatedTask) { - var timeEntry = new TimeEntry(task, time, comment); + var timeEntry = new TimeEntry(task, time, comment, associatedTask); _localTimeRepository.RecordTime(timeEntry); } - public void RecordTime(Task task, int intervalCount, int minutesWorked, DateTime offset, string comment) + public void RecordTime(Task task, int intervalCount, int minutesWorked, DateTime offset, string comment, Task associatedTask) { var totalMinutes = intervalCount*NaggingInterval; var minutesOfBreak = totalMinutes - minutesWorked; var recordTime = offset.AddMinutes(minutesOfBreak); - RecordTime(task, recordTime, comment); + RecordTime(task, recordTime, comment, associatedTask); } public void RecordMarker(DateTime time) @@ -101,6 +101,11 @@ public IEnumerable GetRecentlyRecordedTaskIds(int limit) return _localTimeRepository.GetRecentlyRecordedTaskIds(limit); } + public IEnumerable GetRecentlyAssociatedTaskIds(int limit, Task task) + { + return task?.Project?.Id == null ? new List() : _localTimeRepository.GetRecentlyAssociatedTaskIds(limit, task.Project.Id); + } + public IEnumerable GetRecentlyRecordedCommentsForTask(int limit, Task task) { return task?.Id == null @@ -147,6 +152,22 @@ public void DailyTimeOperations(bool force = false) _settingsService.SaveSetting("LastSyncedDate", DateTime.Now.ToString()); } + List SyncUnsyncedAssociatedEntries() + { + var unsyncedIdString = _settingsService.GetSetting("UnsyncedEntries"); + if(unsyncedIdString.IsNullOrWhitespace()) return new List(); + + var previouslyUnsyncedProjectEntries = unsyncedIdString.Split(',').Select(int.Parse).ToList(); + var unsyncedEntries = _localTimeRepository.GetTimeEntries(previouslyUnsyncedProjectEntries); + var unsyncedAssociatedEntries = new List(); + + foreach (var entry in unsyncedEntries) + { + if(!LogWithAssociatedRepository(entry)) unsyncedAssociatedEntries.Add(entry.Id); + } + return unsyncedAssociatedEntries; + } + public void SyncWithRemote() { // only sync if this feature is enabled @@ -156,14 +177,29 @@ public void SyncWithRemote() // get the unsynced entries var unsyncedEntries = _localTimeRepository.GetUnsyncedEntries(); + var unsyncedAssociatedEntries = SyncUnsyncedAssociatedEntries(); + // loop through each and record in the remote repo foreach (var entry in unsyncedEntries) { if (!_remoteTimeRepository.RecordTime(entry)) continue; + if (entry.HasAssociatedTask) + { + if(!LogWithAssociatedRepository(entry)) unsyncedAssociatedEntries.Add(entry.Id); + } entry.Synced = true; _localTimeRepository.UpdateSyncedOnTimeEntry(entry); } + + _settingsService.SaveSetting("UnsyncedEntries", string.Join(",", unsyncedAssociatedEntries)); + } + + bool LogWithAssociatedRepository(TimeEntry entry) + { + var projectTimeRepository = _associatedRemoteRepositoryService.GetAssociatedRemoteTimeRepository(entry.Project); + if (projectTimeRepository == null) return false; + return projectTimeRepository.RecordAssociatedTime(entry); } public void SquashTime() diff --git a/src/Nagger/Program.cs b/src/Nagger/Program.cs index 63abccc..a93e27c 100644 --- a/src/Nagger/Program.cs +++ b/src/Nagger/Program.cs @@ -8,18 +8,13 @@ using Data.Local; using Data.Meazure; using Interfaces; + using Models; using Quartz; using Quartz.Impl; using Services; using Services.JIRA; using Services.Meazure; - internal enum SupportedRemoteRepository - { - Jira, - Meazure - } - internal class Program { static bool _running; @@ -58,20 +53,48 @@ static void RegisterConditionalComponents(IContainer container) var updater = new ContainerBuilder(); var primaryRepository = GetPrimaryRemoteRepository(); + Action registerMeazure = (builder, isDefault) => + { + builder.RegisterType().As(); + builder.RegisterType().Keyed(SupportedRemoteRepository.Meazure); + builder.RegisterType().Keyed(SupportedRemoteRepository.Meazure); + + if (isDefault) + { + builder.RegisterType().As(); + builder.RegisterType().As(); + } + + builder.RegisterType().As(); + }; + + Action registerJira = (builder, isDefault) => + { + builder.RegisterType().As(); + builder.RegisterType().Keyed(SupportedRemoteRepository.Jira); + builder.RegisterType().Keyed(SupportedRemoteRepository.Jira); + if (isDefault) + { + builder.RegisterType().As(); + builder.RegisterType() + .As() + .As(); + } + builder.RegisterType(); + builder.RegisterType().As(); + }; + if (primaryRepository == SupportedRemoteRepository.Meazure) { - updater.RegisterType().As(); - updater.RegisterType().As(); - updater.RegisterType().As(); - updater.RegisterType().As(); + // meazure is the default so regsiter last + registerJira(updater, false); + registerMeazure(updater, true); } else if (primaryRepository == SupportedRemoteRepository.Jira) { - updater.RegisterType().As(); - updater.RegisterType().As(); - updater.RegisterType().As(); - updater.RegisterType(); - updater.RegisterType().As(); + // jira is the default so register last + registerMeazure(updater, false); + registerJira(updater, true); } updater.Update(container); @@ -81,10 +104,10 @@ static void RegisterFinalComponents(IContainer container) { var updater = new ContainerBuilder(); + updater.RegisterType().As(); updater.RegisterType().As(); updater.RegisterType().As(); updater.RegisterType().As(); - updater.RegisterType().As(); updater.Update(container); @@ -101,6 +124,7 @@ static void FinalizeIocContainer() { RegisterConditionalComponents(Container); RegisterFinalComponents(Container); + InitializeComponents(); } static void Schedule() @@ -205,6 +229,18 @@ static SupportedRemoteRepository GetPrimaryRemoteRepository() } } + static void InitializeComponents() + { + using (var scope = Container.BeginLifetimeScope()) + { + var components = scope.Resolve>(); + foreach (var component in components) + { + component.Initialize(); + } + } + } + static void Initialize() { using (var scope = Container.BeginLifetimeScope())