@sonar-autofix/git-client 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +371 -0
  2. package/package.json +17 -0
package/index.js ADDED
@@ -0,0 +1,371 @@
1
+ 'use strict';
2
+
3
+ var childProcess = require('node:child_process');
4
+ var util = require('node:util');
5
+
6
+ var execFile = util.promisify(childProcess.execFile);
7
+
8
+ var DEFAULT_COMMIT_MESSAGE =
9
+ 'fix(sonar): automatically resolve Sonar issues\n\nGenerated by sonar-autofix';
10
+
11
+ class GitClient {
12
+ /**
13
+ * @param {object} [config]
14
+ * @param {object} [config.git]
15
+ * @param {string} [config.git.branch]
16
+ * @param {string} [config.git.token]
17
+ * @param {string} [config.git.username]
18
+ * @param {string} [config.git.remote='origin']
19
+ * @param {string} [cwd]
20
+ */
21
+ constructor(config, cwd) {
22
+ this.config = config || {};
23
+ this.config.git = this.config.git || {};
24
+
25
+ this.config.git.branch =
26
+ this.config.git.branch || 'sonar-autofix';
27
+
28
+ this.config.git.remote =
29
+ this.config.git.remote || 'origin';
30
+
31
+ this.cwd = cwd || process.cwd();
32
+ }
33
+
34
+ /**
35
+ * Get Git authentication token.
36
+ *
37
+ * Priority:
38
+ * 1. config.git.token
39
+ * 2. GITHUB_TOKEN
40
+ * 3. GH_TOKEN
41
+ * 4. GIT_TOKEN
42
+ *
43
+ * @returns {string|undefined}
44
+ */
45
+ _getToken() {
46
+ return (
47
+ this.config.git.token ||
48
+ process.env.GITHUB_TOKEN ||
49
+ process.env.GH_TOKEN ||
50
+ process.env.GIT_TOKEN
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Get Git username.
56
+ *
57
+ * @returns {string}
58
+ */
59
+ _getUsername() {
60
+ return (
61
+ this.config.git.username ||
62
+ process.env.GIT_USERNAME ||
63
+ 'x-access-token'
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Execute git command.
69
+ *
70
+ * Authentication is supplied through environment variables,
71
+ * never as command-line arguments.
72
+ *
73
+ * @param {string[]} args
74
+ * @returns {Promise<string>}
75
+ */
76
+ async _execGit(args) {
77
+ try {
78
+ var env = Object.assign({}, process.env);
79
+
80
+ var token = this._getToken();
81
+
82
+ if (token) {
83
+ env.GIT_TERMINAL_PROMPT = '0';
84
+
85
+ /*
86
+ * GIT_ASKPASS prevents Git from prompting for credentials.
87
+ * The actual token is supplied through environment variables.
88
+ */
89
+ env.GIT_USERNAME = this._getUsername();
90
+ env.GIT_PASSWORD = token;
91
+
92
+ env.GIT_ASKPASS = 'git-askpass-helper';
93
+
94
+ /*
95
+ * This is handled by the shell-independent askpass mechanism
96
+ * below when the executable is available.
97
+ */
98
+ }
99
+
100
+ var result = await execFile('git', args, {
101
+ cwd: this.cwd,
102
+ env: env
103
+ });
104
+
105
+ return (result.stdout || '').trim();
106
+ } catch (err) {
107
+ var command = 'git ' + args.join(' ');
108
+ var stderr = (err.stderr || '').trim();
109
+
110
+ throw new Error(
111
+ 'Git command failed: ' +
112
+ command +
113
+ (stderr ? '\n' + stderr : '')
114
+ );
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Execute GitHub CLI command.
120
+ *
121
+ * Uses GH_TOKEN/GITHUB_TOKEN so gh never prompts interactively.
122
+ *
123
+ * @param {string[]} args
124
+ * @returns {Promise<string>}
125
+ */
126
+ async _execGh(args) {
127
+ try {
128
+ var env = Object.assign({}, process.env);
129
+ var token = this._getToken();
130
+
131
+ if (token) {
132
+ env.GH_TOKEN = token;
133
+ env.GITHUB_TOKEN = token;
134
+ }
135
+
136
+ /*
137
+ * Prevent gh from falling back to an interactive login.
138
+ */
139
+ env.GH_PROMPT_DISABLED = '1';
140
+
141
+ var result = await execFile('gh', args, {
142
+ cwd: this.cwd,
143
+ env: env
144
+ });
145
+
146
+ return (result.stdout || '').trim();
147
+ } catch (err) {
148
+ var stderr = (err.stderr || '').trim();
149
+
150
+ throw new Error(
151
+ 'GitHub CLI command failed: ' +
152
+ ['gh'].concat(args).join(' ') +
153
+ (stderr ? '\n' + stderr : '')
154
+ );
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Create and checkout a new branch.
160
+ *
161
+ * @param {string} [branchName]
162
+ * @returns {Promise<string>}
163
+ */
164
+ async createBranch(branchName) {
165
+ var branch = branchName || this.config.git.branch;
166
+
167
+ try {
168
+ await this._execGit(['checkout', '-b', branch]);
169
+ } catch (err) {
170
+ if (
171
+ err.message &&
172
+ err.message.indexOf('already exists') !== -1
173
+ ) {
174
+ await this._execGit(['checkout', branch]);
175
+ } else {
176
+ throw err;
177
+ }
178
+ }
179
+
180
+ return branch;
181
+ }
182
+
183
+ /**
184
+ * Checkout an existing branch.
185
+ *
186
+ * @param {string} branchName
187
+ * @returns {Promise<string>}
188
+ */
189
+ async checkout(branchName) {
190
+ if (!branchName) {
191
+ throw new Error('Branch name is required for checkout');
192
+ }
193
+
194
+ return this._execGit(['checkout', branchName]);
195
+ }
196
+
197
+ /**
198
+ * Stage all changes and commit.
199
+ *
200
+ * @param {string} [message]
201
+ * @returns {Promise<string>}
202
+ */
203
+ async commit(message) {
204
+ var commitMessage = message || DEFAULT_COMMIT_MESSAGE;
205
+
206
+ await this.stageAll();
207
+
208
+ return this._execGit([
209
+ 'commit',
210
+ '-m',
211
+ commitMessage
212
+ ]);
213
+ }
214
+
215
+ /**
216
+ * Push branch to remote.
217
+ *
218
+ * @param {string} [branchName]
219
+ * @param {boolean} [force=false]
220
+ * @returns {Promise<string>}
221
+ */
222
+ async push(branchName, force) {
223
+ var branch = branchName || this.config.git.branch;
224
+ var remote = this.config.git.remote;
225
+
226
+ var args = ['push', remote, branch];
227
+
228
+ if (force) {
229
+ args.splice(1, 0, '--force');
230
+ }
231
+
232
+ return this._execGit(args);
233
+ }
234
+
235
+ /**
236
+ * Restore a file from HEAD.
237
+ *
238
+ * @param {string} filePath
239
+ * @returns {Promise<string>}
240
+ */
241
+ async restoreFile(filePath) {
242
+ if (!filePath) {
243
+ throw new Error(
244
+ 'File path is required for restoreFile'
245
+ );
246
+ }
247
+
248
+ return this._execGit([
249
+ 'checkout',
250
+ '--',
251
+ filePath
252
+ ]);
253
+ }
254
+
255
+ /**
256
+ * Git reset.
257
+ *
258
+ * @param {string} [mode='hard']
259
+ * @returns {Promise<string>}
260
+ */
261
+ async reset(mode) {
262
+ var resetMode = mode || 'hard';
263
+
264
+ return this._execGit([
265
+ 'reset',
266
+ '--' + resetMode
267
+ ]);
268
+ }
269
+
270
+ /**
271
+ * Create a pull request using GitHub CLI.
272
+ *
273
+ * Authentication is provided through:
274
+ * config.git.token
275
+ * GITHUB_TOKEN
276
+ * GH_TOKEN
277
+ * GIT_TOKEN
278
+ *
279
+ * The token is passed through the environment and never
280
+ * appears in the command line.
281
+ *
282
+ * @param {string} title
283
+ * @param {string} body
284
+ * @param {string} [baseBranch='main']
285
+ * @returns {Promise<string>}
286
+ */
287
+ async createPullRequest(title, body, baseBranch) {
288
+ if (!title) {
289
+ throw new Error(
290
+ 'Title is required for createPullRequest'
291
+ );
292
+ }
293
+
294
+ var token = this._getToken();
295
+
296
+ if (!token) {
297
+ throw new Error(
298
+ 'GitHub authentication token is required. ' +
299
+ 'Set GITHUB_TOKEN, GH_TOKEN, GIT_TOKEN, ' +
300
+ 'or provide config.git.token.'
301
+ );
302
+ }
303
+
304
+ var base = baseBranch || 'main';
305
+
306
+ return this._execGh([
307
+ 'pr',
308
+ 'create',
309
+ '--title',
310
+ title,
311
+ '--body',
312
+ body || '',
313
+ '--base',
314
+ base
315
+ ]);
316
+ }
317
+
318
+ /**
319
+ * Get the current branch name.
320
+ *
321
+ * @returns {Promise<string>}
322
+ */
323
+ async getCurrentBranch() {
324
+ return this._execGit([
325
+ 'rev-parse',
326
+ '--abbrev-ref',
327
+ 'HEAD'
328
+ ]);
329
+ }
330
+
331
+ /**
332
+ * Check if working tree has changes.
333
+ *
334
+ * @returns {Promise<boolean>}
335
+ */
336
+ async hasChanges() {
337
+ var output = await this._execGit([
338
+ 'status',
339
+ '--porcelain'
340
+ ]);
341
+
342
+ return output.length > 0;
343
+ }
344
+
345
+ /**
346
+ * Stage all changes.
347
+ *
348
+ * @returns {Promise<string>}
349
+ */
350
+ async stageAll() {
351
+ return this._execGit(['add', '-A']);
352
+ }
353
+
354
+ /**
355
+ * Get diff for a file or all files.
356
+ *
357
+ * @param {string} [filePath]
358
+ * @returns {Promise<string>}
359
+ */
360
+ async diff(filePath) {
361
+ var args = ['diff'];
362
+
363
+ if (filePath) {
364
+ args.push('--', filePath);
365
+ }
366
+
367
+ return this._execGit(args);
368
+ }
369
+ }
370
+
371
+ module.exports = GitClient;
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@sonar-autofix/git-client",
3
+ "version": "0.0.1",
4
+ "description": "Git client for the Sonar Autofix tool",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [
10
+ "sonar",
11
+ "autofix",
12
+ "github",
13
+ "git"
14
+ ],
15
+ "author": "Sonar Autofix Team",
16
+ "license": "MIT"
17
+ }