@sonar-autofix/git-client 0.0.1 → 0.2.2

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 +110 -29
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -13,6 +13,7 @@ class GitClient {
13
13
  * @param {object} [config]
14
14
  * @param {object} [config.git]
15
15
  * @param {string} [config.git.branch]
16
+ * @param {string} [config.git.baseBranch]
16
17
  * @param {string} [config.git.token]
17
18
  * @param {string} [config.git.username]
18
19
  * @param {string} [config.git.remote='origin']
@@ -51,6 +52,18 @@ class GitClient {
51
52
  );
52
53
  }
53
54
 
55
+ /**
56
+ * Get the base branch for pull requests.
57
+ *
58
+ * Returns the configured base branch (config.git.baseBranch),
59
+ * or undefined when it is not set in the configuration.
60
+ *
61
+ * @returns {string|undefined}
62
+ */
63
+ _getBaseBranch() {
64
+ return this.config.git.baseBranch;
65
+ }
66
+
54
67
  /**
55
68
  * Get Git username.
56
69
  *
@@ -74,29 +87,15 @@ class GitClient {
74
87
  * @returns {Promise<string>}
75
88
  */
76
89
  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;
90
+ var env = Object.assign({}, process.env);
91
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
- }
92
+ /*
93
+ * Never hang on an interactive credential prompt.
94
+ * Authentication is supplied per-command (see push()).
95
+ */
96
+ env.GIT_TERMINAL_PROMPT = '0';
99
97
 
98
+ try {
100
99
  var result = await execFile('git', args, {
101
100
  cwd: this.cwd,
102
101
  env: env
@@ -104,8 +103,8 @@ class GitClient {
104
103
 
105
104
  return (result.stdout || '').trim();
106
105
  } catch (err) {
107
- var command = 'git ' + args.join(' ');
108
- var stderr = (err.stderr || '').trim();
106
+ var command = this._redactSecrets('git ' + args.join(' '));
107
+ var stderr = this._redactSecrets((err.stderr || '').trim());
109
108
 
110
109
  throw new Error(
111
110
  'Git command failed: ' +
@@ -115,6 +114,64 @@ class GitClient {
115
114
  }
116
115
  }
117
116
 
117
+ /**
118
+ * Remove any configured secrets from a string so they never leak
119
+ * into logs or error messages.
120
+ *
121
+ * @param {string} value
122
+ * @returns {string}
123
+ */
124
+ _redactSecrets(value) {
125
+ if (!value) {
126
+ return value;
127
+ }
128
+
129
+ var token = this._getToken();
130
+
131
+ if (token && value.indexOf(token) !== -1) {
132
+ value = value.split(token).join('[REDACTED]');
133
+ }
134
+
135
+ return value;
136
+ }
137
+
138
+ /**
139
+ * Build an authenticated push URL by embedding the GitHub token.
140
+ *
141
+ * This lets Git authenticate with the token directly instead of
142
+ * prompting for a username and password. Only HTTP(S) remotes are
143
+ * eligible; other remotes (e.g. SSH) return null and fall back to a
144
+ * normal push.
145
+ *
146
+ * @returns {Promise<string|null>}
147
+ */
148
+ async _getAuthenticatedUrl() {
149
+ var token = this._getToken();
150
+
151
+ if (!token) {
152
+ return null;
153
+ }
154
+
155
+ var url;
156
+ try {
157
+ url = await this._execGit(['remote', 'get-url', this.config.git.remote]);
158
+ } catch (err) {
159
+ return null;
160
+ }
161
+
162
+ var match = url.match(/^https?:\/\/(?:[^@\/]+@)?([^\/]+?)\/(.+)$/);
163
+
164
+ if (!match) {
165
+ return null;
166
+ }
167
+
168
+ var host = match[1];
169
+ var path = match[2];
170
+ var user = this._getUsername();
171
+
172
+ return 'https://' + user + ':' + token + '@' + host + '/' + path;
173
+ }
174
+
118
175
  /**
119
176
  * Execute GitHub CLI command.
120
177
  *
@@ -140,7 +197,9 @@ class GitClient {
140
197
 
141
198
  var result = await execFile('gh', args, {
142
199
  cwd: this.cwd,
143
- env: env
200
+ env: env,
201
+ encoding: 'utf8',
202
+ stdio: 'pipe',
144
203
  });
145
204
 
146
205
  return (result.stdout || '').trim();
@@ -150,7 +209,8 @@ class GitClient {
150
209
  throw new Error(
151
210
  'GitHub CLI command failed: ' +
152
211
  ['gh'].concat(args).join(' ') +
153
- (stderr ? '\n' + stderr : '')
212
+ (stderr ? '\n' + stderr : '') + '-' +
213
+ err.message
154
214
  );
155
215
  }
156
216
  }
@@ -215,6 +275,11 @@ class GitClient {
215
275
  /**
216
276
  * Push branch to remote.
217
277
  *
278
+ * When a GitHub token is available and the remote is HTTP(S), the
279
+ * branch is pushed to a token-authenticated URL so Git authenticates
280
+ * with the token directly instead of a username/password. For other
281
+ * remotes (e.g. SSH) it falls back to a normal `git push <remote>`.
282
+ *
218
283
  * @param {string} [branchName]
219
284
  * @param {boolean} [force=false]
220
285
  * @returns {Promise<string>}
@@ -223,10 +288,18 @@ class GitClient {
223
288
  var branch = branchName || this.config.git.branch;
224
289
  var remote = this.config.git.remote;
225
290
 
226
- var args = ['push', remote, branch];
291
+ var args = ['push'];
227
292
 
228
293
  if (force) {
229
- args.splice(1, 0, '--force');
294
+ args.push('--force');
295
+ }
296
+
297
+ var authUrl = await this._getAuthenticatedUrl();
298
+
299
+ if (authUrl) {
300
+ args.push(authUrl, branch);
301
+ } else {
302
+ args.push(remote, branch);
230
303
  }
231
304
 
232
305
  return this._execGit(args);
@@ -279,9 +352,14 @@ class GitClient {
279
352
  * The token is passed through the environment and never
280
353
  * appears in the command line.
281
354
  *
355
+ * The base branch is resolved in the following order:
356
+ * 1. The baseBranch argument (when provided by the caller).
357
+ * 2. config.git.baseBranch from the configuration file.
358
+ * 3. The current git branch (the branch the CLI is running in).
359
+ *
282
360
  * @param {string} title
283
361
  * @param {string} body
284
- * @param {string} [baseBranch='main']
362
+ * @param {string} [baseBranch]
285
363
  * @returns {Promise<string>}
286
364
  */
287
365
  async createPullRequest(title, body, baseBranch) {
@@ -301,7 +379,10 @@ class GitClient {
301
379
  );
302
380
  }
303
381
 
304
- var base = baseBranch || 'main';
382
+ var base =
383
+ baseBranch ||
384
+ this._getBaseBranch() ||
385
+ (await this.getCurrentBranch());
305
386
 
306
387
  return this._execGh([
307
388
  'pr',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sonar-autofix/git-client",
3
- "version": "0.0.1",
3
+ "version": "0.2.2",
4
4
  "description": "Git client for the Sonar Autofix tool",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -14,4 +14,4 @@
14
14
  ],
15
15
  "author": "Sonar Autofix Team",
16
16
  "license": "MIT"
17
- }
17
+ }