diff --git a/package-lock.json b/package-lock.json index 5992c1a..8082c73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "gitdiff", "version": "1.0.0", "dependencies": { + "@types/lodash": "^4.17.15", + "commander": "^13.1.0", + "lodash": "^4.17.21", "minimatch": "^10.0.1", "rimraf": "^6.0.1" }, @@ -1182,6 +1185,12 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.17.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", @@ -1649,6 +1658,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3092,6 +3110,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", diff --git a/package.json b/package.json index dd6b169..787e11d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "typescript": "^5.3.3" }, "dependencies": { + "@types/lodash": "^4.17.15", + "commander": "^13.1.0", + "lodash": "^4.17.21", "minimatch": "^10.0.1", "rimraf": "^6.0.1" } diff --git a/src/diflow.test.ts b/src/diflow.test.ts index 713bdd0..b257032 100644 --- a/src/diflow.test.ts +++ b/src/diflow.test.ts @@ -21,7 +21,7 @@ describe('Git Repository Tests', () => { await beforeDiflow(); - const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos')); + const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos'), 'master'); await processor.process(); await afterDiflow(); @@ -45,7 +45,7 @@ describe('Git Repository Tests', () => { await beforeDiflow(); - const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos')); + const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos'), 'master'); await processor.process(); await afterDiflow(); @@ -72,7 +72,7 @@ describe('Git Repository Tests', () => { await beforeDiflow(); - const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos')); + const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos'), 'master'); await processor.process(); await afterDiflow(); @@ -103,7 +103,7 @@ describe('Git Repository Tests', () => { await beforeDiflow(); - const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos')); + const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos'), 'master'); await processor.process(); await afterDiflow(); @@ -132,7 +132,7 @@ describe('Git Repository Tests', () => { await beforeDiflow(); - const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos')); + const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos'), 'master'); await processor.process(); await afterDiflow(); @@ -147,7 +147,7 @@ describe('Git Repository Tests', () => { expect(content3).toBe('content3'); }); - test.only('Ignore path', async () => { + test('Ignore path', async () => { const folder = path.join(getTestRepoPath('diff'), '.github', 'workflows'); await fs.mkdir(folder, { recursive: true }); await createTestCommit( @@ -159,7 +159,7 @@ describe('Git Repository Tests', () => { await beforeDiflow(); - const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos')); + const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos'), 'master'); await processor.process(); await afterDiflow(); diff --git a/src/diflow.ts b/src/diflow.ts index 6664bd1..b7be85d 100644 --- a/src/diflow.ts +++ b/src/diflow.ts @@ -1,15 +1,39 @@ import path from 'path'; import { Processor } from './processor'; +import { Command } from 'commander'; -if (process.argv.length < 3) { - console.error('Usage: gitdiff '); - process.exit(1); -} +const program = new Command(); -const skipPush = process.argv.includes('--skip-push'); -const clear = process.argv.includes('--clear'); +program + .name('diflow') + .description('Git diflow - maintain sync between GIT 3 repos (base+diff=merged)') + .version('1.0.0'); -const processor = new Processor(process.argv[2], path.join(__dirname, 'workrepos'), { skipPush, clear }); -processor.process(); +program + .command('sync') + .description('Ryn sync between GIT 3 repos (base+diff=merged)') + .requiredOption('-r, --repo', 'URL to control repo repo (configuration+state)') + .requiredOption('-b, --branch', 'Branch name to be processed') + .option('--skip-push', 'skip pushing changes to remote') + .option('--clear', 'clear work repos before running') + .action(options => { + const processor = new Processor(options.config, options.branch, path.join(__dirname, 'workrepos'), { + skipPush: options.skipPush, + clear: options.clear, + }); + processor.process(); + console.log('Processing complete.'); + }); + +// if (process.argv.length < 3) { +// console.error('Usage: gitdiff '); +// process.exit(1); +// } + +// const skipPush = process.argv.includes('--skip-push'); +// const clear = process.argv.includes('--clear'); + +// const processor = new Processor(process.argv[2], path.join(__dirname, 'workrepos'), { skipPush, clear }); +// processor.process(); console.log('Processing complete.'); diff --git a/src/processor.ts b/src/processor.ts index b1333b6..f9a87c5 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -21,12 +21,27 @@ export interface ProcessOptions { clear?: boolean; } +interface CommitToProcess { + commit: string; + ts: number; + authorName: string; + authorEmail: string; + message: string; + authorDate: string; + repoid: RepoId; +} export class Processor { repoPaths: Record; + commitsToProcess: CommitToProcess[] = []; config?: Config = undefined; - constructor(public configRepoUrl: string, public basePath: string, public processOptions: ProcessOptions = {}) { + constructor( + public configRepoUrl: string, + public basePath: string, + public branch: string, + public processOptions: ProcessOptions = {} + ) { this.repoPaths = { base: path.join(this.basePath, 'base'), diff: path.join(this.basePath, 'diff'), @@ -49,7 +64,7 @@ export class Processor { } await cloneRepository(this.repoPaths.config, this.configRepoUrl); - await runGitCommand(this.repoPaths.config, 'checkout master'); + await runGitCommand(this.repoPaths.config, `checkout ${this.branch}`); const configPath = path.join(this.repoPaths.config, 'config.json'); if (!(await fs.exists(configPath))) { @@ -59,6 +74,13 @@ export class Processor { try { this.config = JSON.parse(await fs.readFile(configPath, 'utf8')); + if (!this.config) { + console.error('Invalid configuration file:', configPath); + process.exit(1); + } + if (!this.config.syncCommitPrefix) { + this.config.syncCommitPrefix = 'SYNC:'; + } } catch (err) { console.error('Error parsing config.json:', err); process.exit(1); @@ -67,85 +89,43 @@ export class Processor { await cloneRepository(this.repoPaths.base, this.config!.repos.base); await cloneRepository(this.repoPaths.diff, this.config!.repos.diff); await cloneRepository(this.repoPaths.merged, this.config!.repos.merged); - } - async loadState(): Promise { - const statePath = path.join(this.repoPaths.config, 'state.json'); - if (!(await fs.exists(statePath))) { - console.error(`Missing state file: ${statePath}`); - process.exit(1); - } - - try { - const res: State = JSON.parse(fs.readFileSync(statePath, 'utf8')); - for (const branch of this.config!.branches) { - if (!res['base']?.[branch]?.lastProcessed) { - throw new Error(`Missing state for branch ${branch} in base repo`); - } - if (!res['diff']?.[branch]?.lastProcessed) { - throw new Error(`Missing state for branch ${branch} in diff repo`); - } - if (!res['merged']?.[branch]?.lastProcessed) { - throw new Error(`Missing state for branch ${branch} in merged repo`); - } - // if (!res['base']?.[branch]?.committedByDiflow) { - // res['base'][branch].committedByDiflow = []; - // } - // if (!res['diff']?.[branch]?.committedByDiflow) { - // res['diff'][branch].committedByDiflow = []; - // } - // if (!res['merged']?.[branch]?.committedByDiflow) { - // res['merged'][branch].committedByDiflow = []; - // } - } - return res; - } catch (err) { - console.error('Error parsing state.json:', err); - process.exit(1); - } - } + await runGitCommand(this.repoPaths.base, `checkout ${this.branch}`); + await runGitCommand(this.repoPaths.diff, `checkout ${this.branch}`); + await runGitCommand(this.repoPaths.merged, `checkout ${this.branch}`); - async process() { - await this.initialize(); - - for (const branch of this.config!.branches) { - const branchProcessor = new BranchProcessor(this, branch); - await branchProcessor.process(); - } + await this.readCommitsToProcess(); } -} - -interface CommitToProcess { - commit: string; - ts: number; - authorName: string; - authorEmail: string; - message: string; - authorDate: string; - repoid: RepoId; -} - -class BranchProcessor { - commitsToProcess: CommitToProcess[] = []; - - constructor(public processor: Processor, public branch: string) {} - - async initialize() { - console.log('Initializing branch:', this.branch); - await runGitCommand(this.processor.repoPaths.base, `checkout ${this.branch}`); - await runGitCommand(this.processor.repoPaths.diff, `checkout ${this.branch}`); - await runGitCommand(this.processor.repoPaths.merged, `checkout ${this.branch}`); + async readCommitsToProcess() { console.log('Getting commits...'); - const baseCommits = await getCommits(this.processor.repoPaths.base, this.branch); - const diffCommits = await getCommits(this.processor.repoPaths.diff, this.branch); - const mergedCommits = await getCommits(this.processor.repoPaths.merged, this.branch); - - const state = await this.processor.loadState(); - - const baseFilteredCommits = filterCommitsToProcess(baseCommits, state, this.branch, 'base'); - const diffFilteredCommits = filterCommitsToProcess(diffCommits, state, this.branch, 'diff'); - const mergedFilteredCommits = filterCommitsToProcess(mergedCommits, state, this.branch, 'merged'); + const baseCommits = await getCommits(this.repoPaths.base, this.branch); + const diffCommits = await getCommits(this.repoPaths.diff, this.branch); + const mergedCommits = await getCommits(this.repoPaths.merged, this.branch); + + const state = await this.loadState(); + + const baseFilteredCommits = filterCommitsToProcess( + baseCommits, + state, + this.branch, + 'base', + this.config!.syncCommitPrefix! + ); + const diffFilteredCommits = filterCommitsToProcess( + diffCommits, + state, + this.branch, + 'diff', + this.config!.syncCommitPrefix! + ); + const mergedFilteredCommits = filterCommitsToProcess( + mergedCommits, + state, + this.branch, + 'merged', + this.config!.syncCommitPrefix! + ); this.commitsToProcess = [ ...baseFilteredCommits.map(x => ({ ...x, repoid: 'base' as RepoId })), @@ -157,12 +137,35 @@ class BranchProcessor { console.log('Commits to process:', this.commitsToProcess.length); } + async loadState(): Promise { + const statePath = path.join(this.repoPaths.config, 'state.json'); + let state: State = {}; + if (await fs.exists(statePath)) { + try { + state = JSON.parse(fs.readFileSync(statePath, 'utf8')); + } catch (err) { + console.error('Error parsing state.json:', err); + process.exit(1); + } + } + if (!state['base']?.lastProcessed) { + state['base'] = { ...state['base'], lastProcessed: await getLastCommitHash(this.repoPaths.base) }; + } + if (!state['diff']?.lastProcessed) { + state['diff'] = { ...state['diff'], lastProcessed: await getLastCommitHash(this.repoPaths.diff) }; + } + if (!state['merged']?.lastProcessed) { + state['merged'] = { ...state['merged'], lastProcessed: await getLastCommitHash(this.repoPaths.merged) }; + } + return state; + } + async process() { await this.initialize(); for (const commit of this.commitsToProcess) { console.log('Processing commit', commit.repoid, ':', commit.commit); - const commitProcessor = new CommitProcessor(this.processor, this, commit); + const commitProcessor = new CommitProcessor(this, commit); await commitProcessor.process(); } } @@ -171,7 +174,7 @@ class BranchProcessor { class CommitProcessor { state?: State = undefined; - constructor(public processor: Processor, public branchProcessor: BranchProcessor, public commit: CommitToProcess) {} + constructor(public processor: Processor, public commit: CommitToProcess) {} async initialize() { this.state = await this.processor.loadState(); @@ -186,9 +189,9 @@ class CommitProcessor { } async checkout() { - await runGitCommand(this.processor.repoPaths.base, `checkout ${this.branchProcessor.branch}`); - await runGitCommand(this.processor.repoPaths.diff, `checkout ${this.branchProcessor.branch}`); - await runGitCommand(this.processor.repoPaths.merged, `checkout ${this.branchProcessor.branch}`); + await runGitCommand(this.processor.repoPaths.base, `checkout ${this.processor.branch}`); + await runGitCommand(this.processor.repoPaths.diff, `checkout ${this.processor.branch}`); + await runGitCommand(this.processor.repoPaths.merged, `checkout ${this.processor.branch}`); await runGitCommand(this.processor.repoPaths[this.commit.repoid], `checkout ${this.commit.commit}`); } @@ -247,16 +250,15 @@ class CommitProcessor { await runGitCommand(this.processor.repoPaths[repoid], `add -A`); await runGitCommand( this.processor.repoPaths[repoid], - `commit -m "SYNC: ${this.commit.message}" --author="${this.commit.authorName} <${this.commit.authorEmail}>" --date="${this.commit.authorDate}"` + `commit -m "${this.processor.config?.syncCommitPrefix} ${this.commit.message}" --author="${this.commit.authorName} <${this.commit.authorEmail}>" --date="${this.commit.authorDate}"` ); if (!this.processor.processOptions.skipPush) { await runGitCommand(this.processor.repoPaths[repoid], `push`); } - if (repoid !== 'config') { - const hash = await getLastCommitHash(this.processor.repoPaths[repoid]); - this.state![repoid][this.branchProcessor.branch].lastProcessed = hash; - // this.state![repoid][this.branchProcessor.branch].committedByDiflow.push(hash); - } + // if (repoid !== 'config') { + // const hash = await getLastCommitHash(this.processor.repoPaths[repoid]); + // this.state![repoid].lastProcessed = hash; + // } console.log('Commiting changes for repo:', repoid, 'DONE.'); } } @@ -271,7 +273,7 @@ class CommitProcessor { if (this.commit.repoid !== 'merged') { await this.commitChangesInRepo('merged'); } - this.state![this.commit.repoid][this.branchProcessor.branch].lastProcessed = this.commit.commit; + this.state![this.commit.repoid].lastProcessed = this.commit.commit; await this.saveState(); diff --git a/src/runtest.ts b/src/runtest.ts index fa6da41..50112ce 100644 --- a/src/runtest.ts +++ b/src/runtest.ts @@ -10,7 +10,7 @@ async function main() { break; case 'add': await createTestCommit(getTestRepoPath('diff'), 'newfile.txt', 'new content', 'diff'); - const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos')); + const processor = new Processor(getTestRepoPath('config'), path.join(__dirname, 'workrepos'), 'master'); await processor.process(); break; } diff --git a/src/testrepo.ts b/src/testrepo.ts index 36dfc6c..6364520 100644 --- a/src/testrepo.ts +++ b/src/testrepo.ts @@ -1,8 +1,9 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { rimraf } from 'rimraf'; -import { execAsync, getHeadCommitInRepo, sleep } from './tools'; +import { execAsync, getCommits, getHeadCommitInRepo, sleep } from './tools'; import { State } from './types'; +import _ from 'lodash'; export function getTestRepoPath(repo: string) { const repoPath = path.join(__dirname, 'testrepos', repo); @@ -70,19 +71,13 @@ export async function initTestRepos() { const stateContent = JSON.stringify( { base: { - master: { - lastProcessed: baseHash, - }, + lastProcessed: baseHash, }, diff: { - master: { - lastProcessed: diffHash, - }, + lastProcessed: diffHash, }, merged: { - master: { - lastProcessed: mergedHash, - }, + lastProcessed: mergedHash, }, }, null, @@ -113,13 +108,21 @@ export async function afterDiflow() { export async function checkStateInConfig() { const stateContent = await fs.readFile(path.join(getTestRepoPath('config'), 'state.json'), 'utf8'); const state = JSON.parse(stateContent) as State; - const baseHash = await getHeadCommitInRepo(getTestRepoPath('base')); - const diffHash = await getHeadCommitInRepo(getTestRepoPath('diff')); - const mergedHash = await getHeadCommitInRepo(getTestRepoPath('merged')); - expect(state['base']['master'].lastProcessed).toBe(baseHash); - expect(state['diff']['master'].lastProcessed).toBe(diffHash); - expect(state['merged']['master'].lastProcessed).toBe(mergedHash); + const baseHistory = await getCommits(getTestRepoPath('base'), 'master'); + const diffHistory = await getCommits(getTestRepoPath('diff'), 'master'); + const mergedHistory = await getCommits(getTestRepoPath('merged'), 'master'); + + console.log('MERGED HISTORY'); + console.log(mergedHistory); + + const baseHash = _.findLast(baseHistory, x => !x.message.startsWith('SYNC:'))?.commit; + const diffHash = _.findLast(diffHistory, x => !x.message.startsWith('SYNC:'))?.commit; + const mergedHash = _.findLast(mergedHistory, x => !x.message.startsWith('SYNC:'))?.commit; + + expect(state['base'].lastProcessed).toBe(baseHash); + expect(state['diff'].lastProcessed).toBe(diffHash); + expect(state['merged'].lastProcessed).toBe(mergedHash); // expect(state['base']['master'].committedByDiflow).toEqual([]); // expect(state['diff']['master'].committedByDiflow).toEqual([]); diff --git a/src/tools.ts b/src/tools.ts index dda5d1e..5ba6477 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -19,12 +19,12 @@ export async function runGitCommand(repoPath: string, cmd: string): Promise { - const log = await runGitCommand(repoPath, `log ${branch} --reverse --pretty=format:"%H|%ct|%aN|%aE|%f|%ad"`); + const log = await runGitCommand(repoPath, `log ${branch} --reverse --pretty=format:"%H@|@%ct@|@%aN@|@%aE@|@%s@|@%ad"`); const res = log .split('\n') .filter(Boolean) .map(x => { - const [commit, ts, authorName, authorEmail, message, authorDate] = x.split('|'); + const [commit, ts, authorName, authorEmail, message, authorDate] = x.split('@|@'); return { commit, ts: parseInt(ts), @@ -45,14 +45,19 @@ export async function cloneRepository(repoPath: string, url: string) { } } -export function filterCommitsToProcess(commits: Commit[], state: State, branch: string, repoid: RepoId): Commit[] { - const lastCommitIndex = commits.findIndex(x => x.commit === state[repoid][branch].lastProcessed); +export function filterCommitsToProcess( + commits: Commit[], + state: State, + branch: string, + repoid: RepoId, + syncCommitPrefix: string +): Commit[] { + const lastCommitIndex = commits.findIndex(x => x.commit === state[repoid].lastProcessed); if (lastCommitIndex < 0) { console.log(`Could not find last processed commit for ${branch} in ${repoid}`); process.exit(1); } - // return commits.slice(lastCommitIndex + 1).filter(x => !state[repoid][branch].committedByDiflow?.includes(x.commit)); - return commits.slice(lastCommitIndex + 1); + return commits.slice(lastCommitIndex + 1).filter(x => !x.message?.startsWith(syncCommitPrefix)); } export async function getDiffForCommit(repoPath: string, commitHash: string): Promise { diff --git a/src/types.ts b/src/types.ts index 719812f..b5fc07a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,6 @@ export type RepoId = 'base' | 'diff' | 'merged' | 'config'; export type FileAction = 'A' | 'D' | 'M'; export interface Config { - branches: string[]; repos: { base: string; diff: string; @@ -10,14 +9,12 @@ export interface Config { config: string; }; ignorePaths: string[]; + syncCommitPrefix?: string; } export interface State { [repo: string]: { - [branch: string]: { - lastProcessed: string; - // committedByDiflow: string[]; - }; + lastProcessed: string; }; }