Skip to content

Commit 9d0da4e

Browse files
authored
Merge pull request #23 from dlabaj/git-init
fix: fixed issue where git may not be setup on users computer.
2 parents 7394fd5 + 8030b7e commit 9d0da4e

File tree

6 files changed

+212
-22
lines changed

6 files changed

+212
-22
lines changed

src/__tests__/create.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ import { sanitizeRepoName, offerAndCreateGitHubRepo } from '../github.js';
3636
import { runCreate } from '../create.js';
3737
import { defaultTemplates } from '../templates.js';
3838

39-
const mockPathExists = fs.pathExists as jest.MockedFunction<typeof fs.pathExists>;
39+
/** Partial `fs-extra` mock: use `jest.Mock` for `mockResolvedValue` (typed mocks infer `never` here). */
40+
const mockPathExists = fs.pathExists as jest.MockedFunction<typeof fs.pathExists> & jest.Mock;
4041
const mockReadJson = fs.readJson as jest.MockedFunction<typeof fs.readJson>;
4142
const mockWriteJson = fs.writeJson as jest.MockedFunction<typeof fs.writeJson>;
42-
const mockRemove = fs.remove as jest.MockedFunction<typeof fs.remove>;
43+
const mockRemove = fs.remove as jest.MockedFunction<typeof fs.remove> & jest.Mock;
4344
const mockExeca = execa as jest.MockedFunction<typeof execa>;
4445
const mockPrompt = inquirer.prompt as jest.MockedFunction<typeof inquirer.prompt>;
4546
const mockOfferAndCreateGitHubRepo = offerAndCreateGitHubRepo as jest.MockedFunction<
@@ -62,7 +63,7 @@ function setupHappyPathMocks() {
6263
mockReadJson.mockResolvedValue({ name: 'template-name', version: '0.0.0' });
6364
mockWriteJson.mockResolvedValue(undefined);
6465
mockRemove.mockResolvedValue(undefined);
65-
mockOfferAndCreateGitHubRepo.mockResolvedValue(undefined);
66+
mockOfferAndCreateGitHubRepo.mockResolvedValue(false);
6667
mockPrompt.mockResolvedValue(projectData);
6768
return projectData;
6869
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
jest.mock('inquirer', () => ({
2+
__esModule: true,
3+
default: { prompt: jest.fn() },
4+
}));
5+
6+
jest.mock('execa', () => ({
7+
__esModule: true,
8+
execa: jest.fn(),
9+
}));
10+
11+
import inquirer from 'inquirer';
12+
import { execa } from 'execa';
13+
import { promptAndSetLocalGitUser } from '../git-user-config.js';
14+
15+
const mockPrompt = inquirer.prompt as jest.MockedFunction<typeof inquirer.prompt>;
16+
const mockExeca = execa as jest.MockedFunction<typeof execa>;
17+
18+
describe('promptAndSetLocalGitUser', () => {
19+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
20+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
21+
22+
beforeEach(() => {
23+
jest.clearAllMocks();
24+
});
25+
26+
afterAll(() => {
27+
consoleErrorSpy.mockRestore();
28+
consoleLogSpy.mockRestore();
29+
});
30+
31+
it('reads global defaults then sets local user.name and user.email', async () => {
32+
mockExeca
33+
.mockResolvedValueOnce({ stdout: 'Jane Doe\n', stderr: '', exitCode: 0 } as Awaited<ReturnType<typeof execa>>)
34+
.mockResolvedValueOnce({ stdout: 'jane@example.com\n', stderr: '', exitCode: 0 } as Awaited<
35+
ReturnType<typeof execa>
36+
>)
37+
.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 } as Awaited<ReturnType<typeof execa>>);
38+
39+
mockPrompt.mockResolvedValue({
40+
userName: ' Local Name ',
41+
userEmail: ' local@example.com ',
42+
});
43+
44+
await promptAndSetLocalGitUser('/tmp/proj');
45+
46+
expect(mockExeca).toHaveBeenNthCalledWith(1, 'git', ['config', '--global', 'user.name'], {
47+
reject: false,
48+
});
49+
expect(mockExeca).toHaveBeenNthCalledWith(2, 'git', ['config', '--global', 'user.email'], {
50+
reject: false,
51+
});
52+
expect(mockExeca).toHaveBeenNthCalledWith(3, 'git', ['config', '--local', 'user.name', 'Local Name'], {
53+
cwd: '/tmp/proj',
54+
stdio: 'inherit',
55+
});
56+
expect(mockExeca).toHaveBeenNthCalledWith(
57+
4,
58+
'git',
59+
['config', '--local', 'user.email', 'local@example.com'],
60+
{ cwd: '/tmp/proj', stdio: 'inherit' },
61+
);
62+
expect(consoleLogSpy).toHaveBeenCalledWith(
63+
expect.stringContaining('Set local git user.name and user.email'),
64+
);
65+
});
66+
67+
it('skips git config when name or email is empty after trim', async () => {
68+
mockExeca.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 } as Awaited<ReturnType<typeof execa>>);
69+
mockPrompt.mockResolvedValue({ userName: '', userEmail: 'a@b.com' });
70+
71+
await promptAndSetLocalGitUser('/tmp/proj');
72+
73+
const localConfigCalls = mockExeca.mock.calls.filter(
74+
([cmd, args]) =>
75+
cmd === 'git' && Array.isArray(args) && (args as string[]).includes('--local'),
76+
);
77+
expect(localConfigCalls).toHaveLength(0);
78+
expect(consoleErrorSpy).toHaveBeenCalledWith(
79+
expect.stringContaining('Both user.name and user.email are required'),
80+
);
81+
});
82+
});

src/cli.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { runSave } from './save.js';
1313
import { runLoad } from './load.js';
1414
import { runDeployToGitHubPages } from './gh-pages.js';
1515
import { readPackageVersion } from './read-package-version.js';
16+
import { promptAndSetLocalGitUser } from './git-user-config.js';
1617

1718
const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
1819
const packageVersion = readPackageVersion(packageJsonPath);
@@ -42,14 +43,27 @@ program
4243
.command('init')
4344
.description('Initialize the current directory (or path) as a git repo and optionally create a GitHub repository')
4445
.argument('[path]', 'Path to the project directory (defaults to current directory)')
45-
.action(async (dirPath) => {
46-
const cwd = dirPath ? path.resolve(dirPath) : process.cwd();
47-
const gitDir = path.join(cwd, '.git');
48-
if (!(await fs.pathExists(gitDir))) {
49-
await execa('git', ['init'], { stdio: 'inherit', cwd });
50-
console.log('✅ Git repository initialized.\n');
46+
.option('--git-init', 'Prompt for git user.name and user.email and store them locally for this repository')
47+
.action(async (dirPath, options) => {
48+
try {
49+
const cwd = dirPath ? path.resolve(dirPath) : process.cwd();
50+
const gitDir = path.join(cwd, '.git');
51+
if (!(await fs.pathExists(gitDir))) {
52+
await execa('git', ['init'], { stdio: 'inherit', cwd });
53+
console.log('✅ Git repository initialized.\n');
54+
}
55+
if (options.gitInit) {
56+
await promptAndSetLocalGitUser(cwd);
57+
}
58+
await offerAndCreateGitHubRepo(cwd);
59+
} catch (error) {
60+
if (error instanceof Error) {
61+
console.error(`\n❌ ${error.message}\n`);
62+
} else {
63+
console.error(error);
64+
}
65+
process.exit(1);
5166
}
52-
await offerAndCreateGitHubRepo(cwd);
5367
});
5468

5569
/** Command to list all available templates */

src/create.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export async function runCreate(
159159
await execa(packageManager, ['install'], { cwd: projectPath, stdio: 'inherit' });
160160
console.log('✅ Dependencies installed.');
161161

162-
// Optional: Create GitHub repository
162+
// Optional: Create GitHub repository (explains what to check if it does not complete)
163163
await offerAndCreateGitHubRepo(projectPath);
164164

165165
// Let the user know the project was created successfully

src/git-user-config.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { execa } from 'execa';
2+
import inquirer from 'inquirer';
3+
4+
async function getGlobalGitValue(key: 'user.name' | 'user.email'): Promise<string | undefined> {
5+
const result = await execa('git', ['config', '--global', key], { reject: false });
6+
const value = result.stdout?.trim();
7+
return value || undefined;
8+
}
9+
10+
/**
11+
* Prompts for user.name and user.email and sets them locally for the repository at cwd.
12+
* Defaults are taken from global git config when present.
13+
*/
14+
export async function promptAndSetLocalGitUser(cwd: string): Promise<void> {
15+
const defaultName = await getGlobalGitValue('user.name');
16+
const defaultEmail = await getGlobalGitValue('user.email');
17+
18+
const answers = await inquirer.prompt([
19+
{
20+
type: 'input',
21+
name: 'userName',
22+
message: 'Git user.name for this repository:',
23+
default: defaultName ?? '',
24+
},
25+
{
26+
type: 'input',
27+
name: 'userEmail',
28+
message: 'Git user.email for this repository:',
29+
default: defaultEmail ?? '',
30+
},
31+
]);
32+
33+
const name = typeof answers.userName === 'string' ? answers.userName.trim() : '';
34+
const email = typeof answers.userEmail === 'string' ? answers.userEmail.trim() : '';
35+
36+
if (!name || !email) {
37+
console.error('\n⚠️ Both user.name and user.email are required. Git user was not configured.\n');
38+
return;
39+
}
40+
41+
try {
42+
await execa('git', ['config', '--local', 'user.name', name], { cwd, stdio: 'inherit' });
43+
await execa('git', ['config', '--local', 'user.email', email], { cwd, stdio: 'inherit' });
44+
console.log('\n✅ Set local git user.name and user.email for this repository.\n');
45+
} catch {
46+
console.error('\n⚠️ Could not set git config. Ensure git is installed and this directory is a repository.\n');
47+
}
48+
}

src/github.ts

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,53 @@ async function ensureInitialCommit(projectPath: string): Promise<void> {
6161
});
6262
} catch {
6363
await execa('git', ['add', '.'], { stdio: 'inherit', cwd: projectPath });
64-
await execa('git', ['commit', '-m', 'Initial commit'], {
65-
stdio: 'inherit',
66-
cwd: projectPath,
67-
});
64+
try {
65+
await execa('git', ['commit', '-m', 'Initial commit'], {
66+
stdio: 'inherit',
67+
cwd: projectPath,
68+
});
69+
} catch (err) {
70+
const stderr =
71+
err && typeof err === 'object' && 'stderr' in err
72+
? String((err as { stderr?: unknown }).stderr ?? '')
73+
: '';
74+
const msg = err instanceof Error ? err.message : String(err);
75+
const combined = `${msg}\n${stderr}`;
76+
const looksLikeIdentityError =
77+
/author identity unknown|please tell me who you are|unable to auto-detect email address|user\.email is not set|user\.name is not set/i.test(
78+
combined,
79+
);
80+
if (looksLikeIdentityError) {
81+
throw new Error(
82+
'Could not create the initial git commit. Set your git identity, then try again:\n' +
83+
' git config --global user.name "Your Name"\n' +
84+
' git config --global user.email "you@example.com"',
85+
);
86+
}
87+
throw err;
88+
}
6889
}
6990
}
7091

71-
/**
72-
* Create a new GitHub repository and return its URL. Does not push.
92+
/**
93+
* After `create` removes the template `.git`, only a successful GitHub flow adds a repo back.
94+
* Call this when the user asked for GitHub but setup did not finish.
95+
*/
96+
function logGitHubSetupDidNotComplete(projectPath: string): void {
97+
const resolved = path.resolve(projectPath);
98+
console.log('\n⚠️ Git repository setup did not complete.');
99+
console.log(' The template’s .git directory was removed after clone, so this folder is not a git repo yet.\n');
100+
console.log(' Check:');
101+
console.log(' • GitHub CLI: `gh auth status` — if not logged in, run `gh auth login`');
102+
console.log(' • Network and API errors above (permissions, repo name already exists, etc.)');
103+
console.log(
104+
' • Your git user.name and/or user.email may not be set. Run `patternfly-cli init --git-init` in the project directory to set local git identity and try again.',
105+
);
106+
console.log(`\n Project path: ${resolved}\n`);
107+
}
108+
109+
/**
110+
* Create a new GitHub repository and return its URL. Pushes the current branch via `gh repo create --push`.
73111
*/
74112
export async function createRepo(options: {
75113
repoName: string;
@@ -114,7 +152,8 @@ export async function offerAndCreateGitHubRepo(projectPath: string): Promise<boo
114152
{
115153
type: 'confirm',
116154
name: 'createGitHub',
117-
message: 'Would you like to create a GitHub repository for this project?',
155+
message:
156+
'Would you like to create a GitHub repository for this project? (requires GitHub CLI and gh auth login)',
118157
default: false,
119158
},
120159
]);
@@ -124,7 +163,8 @@ export async function offerAndCreateGitHubRepo(projectPath: string): Promise<boo
124163
const auth = await checkGhAuth();
125164
if (!auth.ok) {
126165
console.log(`\n⚠️ ${auth.message}`);
127-
console.log(' Skipping GitHub repository creation.\n');
166+
console.log(' Skipping GitHub repository creation.');
167+
logGitHubSetupDidNotComplete(projectPath);
128168
return false;
129169
}
130170

@@ -149,7 +189,10 @@ export async function offerAndCreateGitHubRepo(projectPath: string): Promise<boo
149189
repoName = sanitizeRepoName(alternativeName.trim());
150190
}
151191

152-
if (!repoName) return false;
192+
if (!repoName) {
193+
logGitHubSetupDidNotComplete(projectPath);
194+
return false;
195+
}
153196

154197
const repoUrl = `https://github.com/${auth.username}/${repoName}`;
155198
console.log('\n📋 The following will happen:\n');
@@ -168,7 +211,8 @@ export async function offerAndCreateGitHubRepo(projectPath: string): Promise<boo
168211
]);
169212

170213
if (!confirmCreate) {
171-
console.log('\n❌ GitHub repository was not created.\n');
214+
console.log('\n❌ GitHub repository was not created.');
215+
logGitHubSetupDidNotComplete(projectPath);
172216
return false;
173217
}
174218

@@ -187,7 +231,8 @@ export async function offerAndCreateGitHubRepo(projectPath: string): Promise<boo
187231
return true;
188232
} catch (err) {
189233
console.error('\n❌ Failed to create GitHub repository:');
190-
if (err instanceof Error) console.error(` ${err.message}\n`);
234+
if (err instanceof Error) console.error(` ${err.message}`);
235+
logGitHubSetupDidNotComplete(projectPath);
191236
return false;
192237
}
193238
}

0 commit comments

Comments
 (0)