@codecovevienna/gittt-cli
Version:
Tracking time with CLI into a git repository
319 lines (277 loc) • 10.4 kB
text/typescript
import fs, { WriteOptions } from "fs-extra";
import path from "path";
import YAML from 'yaml'
import { IConfigFile, IProject, IProjectMeta, ITimerFile, IGitttFile } from "../interfaces";
import { LogHelper, parseProjectNameFromGitUrl, ProjectHelper } from "./";
import { GitttFileError } from '../types/errors/gitttFileError';
export class FileHelper {
public static getHomeDir = (): string => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const home: string | null = require("os").homedir()
|| process.env.HOME
|| process.env.HOMEPATH
|| process.env.USERPROFIL;
if (!home) {
throw new Error("Unable to determinate home directory");
}
return home;
}
private configFilePath: string;
private timerFilePath: string;
private configDir: string;
private projectDir: string;
private configObject: IConfigFile | undefined; // Cache
private jsonWriteOptions: WriteOptions = {
EOL: "\n",
spaces: 4,
};
constructor(configDir: string, configFileName: string, timerFileName: string, projectDir: string) {
this.configDir = configDir;
this.projectDir = path.join(configDir, projectDir);
this.configFilePath = path.join(configDir, configFileName);
this.timerFilePath = path.join(configDir, timerFileName);
}
public createConfigDir = async (): Promise<void> => {
await fs.mkdirs(this.configDir);
await fs.mkdirs(this.projectDir);
}
public getProjectPath(project: IProject): string {
if (!project.meta) {
return path.join(this.projectDir);
} else {
return this.projectMetaToPath(project.meta);
}
}
public initConfigFile = async (gitRepo: string): Promise<IConfigFile> => {
const initial: IConfigFile = {
created: Date.now(),
gitRepo,
links: [],
};
await this.saveConfigObject(initial);
return initial;
}
public initProject = async (project: IProject): Promise<IProject> => {
try {
const projectPath: string = await this.getProjectPath(project);
LogHelper.debug(`Ensuring domain directory for ${project.name}`);
await fs.mkdirs(projectPath);
await this.saveProjectObject(project);
return project;
} catch (err: any) {
LogHelper.debug("Error writing project file", err);
throw new Error("Error initializing project");
}
}
public initTimerFile = async (): Promise<void> => {
try {
const initial: ITimerFile = {
start: 0,
stop: 0,
};
await fs.writeJson(this.timerFilePath, initial, this.jsonWriteOptions);
} catch (err: any) {
LogHelper.debug("Error initializing timer file", err);
throw new Error("Error initializing timer file");
}
}
public configDirExists = async (): Promise<boolean> => {
try {
return await fs.pathExists(this.configFilePath);
} catch (err: any) {
LogHelper.error("Error checking config file existence");
return false;
}
}
public getGitttFile = async (): Promise<IGitttFile> => {
try {
return YAML.parse((await fs
.readFile(".gittt.yml"))
.toString()) as IGitttFile;
} catch (err: any) {
LogHelper.debug("Unable to parse .gittt.yml file", err);
throw new GitttFileError("Unable to parse .gittt.yml file")
}
}
public getConfigObject = async (fromDisk = false): Promise<IConfigFile> => {
try {
if (!this.configObject || fromDisk) {
const configObj: IConfigFile = await fs.readJson(this.configFilePath);
this.setConfigObject(configObj);
return configObj;
} else {
return this.configObject;
}
} catch (err: any) {
LogHelper.debug("Error reading config file", err);
throw new Error("Error getting config object");
}
}
public async isConfigFileValid(): Promise<boolean> {
let config: IConfigFile | undefined;
try {
config = await this.getConfigObject(true);
} catch (err: any) {
LogHelper.debug(`Unable to parse config file: ${err.message}`);
return false;
}
try {
parseProjectNameFromGitUrl(config.gitRepo);
return true;
} catch (err: any) {
LogHelper.debug("Unable to get project name", err);
return false;
}
}
public timerFileExists = (): boolean => {
try {
return fs.existsSync(this.timerFilePath);
} catch (err: any) {
LogHelper.error("Error checking timer file existence");
return false;
}
}
public saveProjectObject = async (project: IProject): Promise<void> => {
try {
const projectPath: string = await this.getProjectPath(project);
const projectFilePath: string = path.join(projectPath, `${project.name}.json`);
LogHelper.debug(`Saving project file to ${projectFilePath}`);
await fs.writeJson(projectFilePath, project, this.jsonWriteOptions);
// TODO update cache
} catch (err: any) {
LogHelper.debug("Error writing project file", err);
throw new Error("Error writing project file");
}
}
public invalidateCache = (): void => {
this.configObject = undefined;
}
public getTimerObject = async (): Promise<ITimerFile> => {
try {
const timerObj: ITimerFile = await fs.readJson(this.timerFilePath);
return timerObj;
} catch (err: any) {
LogHelper.debug("Error reading timer object", err);
throw new Error("Error getting timer object");
}
}
public initReadme = async (): Promise<void> => {
try {
await fs.writeFile(path.join(this.configDir, "README.md"), "# Initially generated gittt README.md file");
} catch (err: any) {
LogHelper.debug("Error writing readme file", err);
throw new Error("Error initializing readme file");
}
}
public findProjectByName = async (projectName: string, projectMeta?: IProjectMeta): Promise<IProject | undefined> => {
const allFoundProjects: IProject[] = [];
if (projectMeta) {
// Use specific domain
const domainProjects: IProject[] = await this.findProjectsForDomain(projectMeta);
for (const project of domainProjects) {
if (project.name === projectName) {
allFoundProjects.push(project);
}
}
} else {
// Search in all domains
const projectDomains: string[] = fs.readdirSync(this.projectDir);
for (const projectDomain of projectDomains) {
const tmpStat = fs.lstatSync(path.join(this.projectDir, projectDomain))
if (tmpStat.isFile()) {
const project: IProject = await fs.readJson(path.join(this.projectDir, projectDomain));
if (project.name === projectName) {
allFoundProjects.push(project);
}
} else {
const projectFiles: string[] = fs.readdirSync(path.join(this.projectDir, projectDomain));
for (const projectFile of projectFiles) {
const project: IProject = await fs.readJson(path.join(this.projectDir, projectDomain, projectFile));
if (project.name === projectName) {
allFoundProjects.push(project);
}
}
}
}
}
switch (allFoundProjects.length) {
case 0:
// No project found
return undefined;
case 1:
// No project found
return allFoundProjects[0];
default:
// If more than 1 project with the given name gets found, throw error
throw new Error(`Found more than 1 project named "${projectName}"`);
}
}
public saveTimerObject = async (timer: ITimerFile): Promise<void> => {
try {
await fs.writeJson(this.timerFilePath, timer, this.jsonWriteOptions);
} catch (err: any) {
LogHelper.debug("Error writing timer file", err);
throw new Error("Error writing timer file");
}
}
public findAllProjects = async (): Promise<IProject[]> => {
const allProjects: IProject[] = [];
const projectDomains: string[] = fs.readdirSync(this.projectDir);
for (const projectDomain of projectDomains) {
const tmpStat = fs.lstatSync(path.join(this.projectDir, projectDomain))
if (tmpStat.isFile()) {
const project: IProject = await fs.readJson(path.join(this.projectDir, projectDomain));
allProjects.push(project);
} else {
const projectFiles: string[] = fs.readdirSync(path.join(this.projectDir, projectDomain));
for (const projectFile of projectFiles) {
const project: IProject = await fs.readJson(path.join(this.projectDir, projectDomain, projectFile));
allProjects.push(project);
}
}
}
return allProjects;
}
public findProjectsForDomain = async (projectMeta: IProjectMeta): Promise<IProject[]> => {
const projects: IProject[] = [];
if (!await fs.pathExists(this.projectMetaToPath(projectMeta))) {
return projects;
}
const projectFiles: string[] = fs.readdirSync(this.projectMetaToPath(projectMeta));
for (const projectFile of projectFiles) {
const project: IProject = await fs.readJson(path.join(this.projectMetaToPath(projectMeta), projectFile));
projects.push(project);
}
return projects;
}
public removeDomainDirectory = async (projectMeta: IProjectMeta, force = false): Promise<void> => {
const projectsInDomain: IProject[] = await this.findProjectsForDomain(projectMeta);
if (projectsInDomain.length > 0) {
if (force) {
await fs.remove(this.projectMetaToPath(projectMeta));
} else {
throw new Error(`${this.projectMetaToPath(projectMeta)} is not empty`);
}
} else {
await fs.remove(this.projectMetaToPath(projectMeta));
}
}
public removeProjectFile = async (project: IProject): Promise<void> => {
return fs.remove(ProjectHelper.projectToProjectFilename(project));
}
private setConfigObject = (config: IConfigFile): void => {
this.configObject = config;
}
private projectMetaToPath = (projectMeta: IProjectMeta): string => {
return path.join(this.projectDir, ProjectHelper.projectMetaToDomain(projectMeta));
}
public saveConfigObject = async (config: IConfigFile): Promise<void> => {
try {
await fs.writeJson(this.configFilePath, config, this.jsonWriteOptions);
this.setConfigObject(config);
} catch (err: any) {
LogHelper.debug("Error writing config file", err);
throw new Error("Error writing config file");
}
}
}