Skip to content

Commit 7045074

Browse files
committed
fix: fixed issue where git may not be setup on users computer.
1 parent 7394fd5 commit 7045074

File tree

6 files changed

+182
-16
lines changed

6 files changed

+182
-16
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+
expect(mockExeca).not.toHaveBeenCalledWith(
74+
'git',
75+
['config', '--local', 'user.name', expect.any(String)],
76+
expect.any(Object),
77+
);
78+
expect(consoleErrorSpy).toHaveBeenCalledWith(
79+
expect.stringContaining('Both user.name and user.email are required'),
80+
);
81+
});
82+
});

src/cli.ts

Lines changed: 6 additions & 1 deletion
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,13 +43,17 @@ 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+
.option('--git-init', 'Prompt for git user.name and user.email and store them locally for this repository')
47+
.action(async (dirPath, options) => {
4648
const cwd = dirPath ? path.resolve(dirPath) : process.cwd();
4749
const gitDir = path.join(cwd, '.git');
4850
if (!(await fs.pathExists(gitDir))) {
4951
await execa('git', ['init'], { stdio: 'inherit', cwd });
5052
console.log('✅ Git repository initialized.\n');
5153
}
54+
if (options.gitInit) {
55+
await promptAndSetLocalGitUser(cwd);
56+
}
5257
await offerAndCreateGitHubRepo(cwd);
5358
});
5459

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: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,38 @@ 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 {
70+
throw new Error(
71+
'Could not create the initial git commit. Set your git identity, then try again:\n' +
72+
' git config --global user.name "Your Name"\n' +
73+
' git config --global user.email "you@example.com"',
74+
);
75+
}
6876
}
6977
}
7078

71-
/**
72-
* Create a new GitHub repository and return its URL. Does not push.
79+
/**
80+
* After `create` removes the template `.git`, only a successful GitHub flow adds a repo back.
81+
* Call this when the user asked for GitHub but setup did not finish.
82+
*/
83+
function logGitHubSetupDidNotComplete(projectPath: string): void {
84+
const resolved = path.resolve(projectPath);
85+
console.log('\n⚠️ Git repository setup did not complete.');
86+
console.log(' The template’s .git directory was removed after clone, so this folder is not a git repo yet.\n');
87+
console.log(' Check:');
88+
console.log(' • GitHub CLI: `gh auth status` — if not logged in, run `gh auth login`');
89+
console.log(' • Network and API errors above (permissions, repo name already exists, etc.)');
90+
console.log(' • Your user name and/or user email my not be set. Run `patternfly-cli init --git-init` in the project directory (this will set the your `user.name` and `user.email` and try to again)');
91+
console.log(`\n Project path: ${resolved}\n`);
92+
}
93+
94+
/**
95+
* Create a new GitHub repository and return its URL. Does not push.
7396
*/
7497
export async function createRepo(options: {
7598
repoName: string;
@@ -114,7 +137,8 @@ export async function offerAndCreateGitHubRepo(projectPath: string): Promise<boo
114137
{
115138
type: 'confirm',
116139
name: 'createGitHub',
117-
message: 'Would you like to create a GitHub repository for this project?',
140+
message:
141+
'Would you like to create a GitHub repository for this project? (requires GitHub CLI and gh auth login)',
118142
default: false,
119143
},
120144
]);
@@ -124,7 +148,8 @@ export async function offerAndCreateGitHubRepo(projectPath: string): Promise<boo
124148
const auth = await checkGhAuth();
125149
if (!auth.ok) {
126150
console.log(`\n⚠️ ${auth.message}`);
127-
console.log(' Skipping GitHub repository creation.\n');
151+
console.log(' Skipping GitHub repository creation.');
152+
logGitHubSetupDidNotComplete(projectPath);
128153
return false;
129154
}
130155

@@ -149,7 +174,10 @@ export async function offerAndCreateGitHubRepo(projectPath: string): Promise<boo
149174
repoName = sanitizeRepoName(alternativeName.trim());
150175
}
151176

152-
if (!repoName) return false;
177+
if (!repoName) {
178+
logGitHubSetupDidNotComplete(projectPath);
179+
return false;
180+
}
153181

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

170198
if (!confirmCreate) {
171-
console.log('\n❌ GitHub repository was not created.\n');
199+
console.log('\n❌ GitHub repository was not created.');
200+
logGitHubSetupDidNotComplete(projectPath);
172201
return false;
173202
}
174203

@@ -187,7 +216,8 @@ export async function offerAndCreateGitHubRepo(projectPath: string): Promise<boo
187216
return true;
188217
} catch (err) {
189218
console.error('\n❌ Failed to create GitHub repository:');
190-
if (err instanceof Error) console.error(` ${err.message}\n`);
219+
if (err instanceof Error) console.error(` ${err.message}`);
220+
logGitHubSetupDidNotComplete(projectPath);
191221
return false;
192222
}
193223
}

0 commit comments

Comments
 (0)