pull-request-split-advisor 3.2.6 → 3.2.7

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/dist/git/git.js +46 -13
  2. package/package.json +1 -1
package/dist/git/git.js CHANGED
@@ -243,40 +243,73 @@ export function findCascadeParent(currentBranch, baseBranch) {
243
243
  if (!familyMatch)
244
244
  return null;
245
245
  const familyPrefix = `${familyMatch[1]}/${familyMatch[2].toUpperCase()}`;
246
- // Construir lista de ramas locales Y remotas para no perder ramas hermanas
247
- // que existen en el remoto pero aún no han sido descargadas localmente.
246
+ // Construir lista de candidatos desde tres fuentes (en orden de precedencia):
247
+ // 1. Ramas locales
248
+ // 2. Refs de tracking remoto ya descargadas (git branch -r)
249
+ // 3. Ramas que existen en el remoto pero nunca fueron descargadas (git ls-remote)
250
+ //
248
251
  // Para cada entrada guardamos { name, ref }:
249
- // - name: nombre de la rama sin prefijo de remoto (para mostrar al usuario y para checkout)
250
- // - ref: ref resolvible por git (local: igual al name; remota: "origin/feature/…")
252
+ // - name: nombre sin prefijo de remoto (para mostrar al usuario y hacer checkout)
253
+ // - ref: ref resolvible por git (local o "origin/branch")
251
254
  const branches = [];
252
- // Ramas locales
255
+ const seenNames = new Set();
256
+ // 1. Ramas locales
253
257
  const localRaw = shSafe("git branch --format=%(refname:short)");
254
258
  for (const b of localRaw.split("\n").map((s) => s.trim()).filter(Boolean)) {
255
259
  branches.push({ name: b, ref: b });
260
+ seenNames.add(b.toUpperCase());
256
261
  }
257
- // Ramas remotas (ej. "origin/feature/FASTY-0001-xyz")
258
- // Se usa el nombre sin el prefijo del remoto (primer segmento hasta "/").
259
- // Si ya existe la rama localmente, se omite (la ref local tiene precedencia).
262
+ // 2. Ramas remotas ya cacheadas localmente (git branch -r)
260
263
  const remoteRaw = shSafe("git branch -r --format=%(refname:short)");
261
264
  for (const b of remoteRaw.split("\n").map((s) => s.trim()).filter(Boolean)) {
262
265
  const slash = b.indexOf("/");
263
266
  if (slash < 0)
264
267
  continue;
265
- const name = b.slice(slash + 1); // "origin/feature/…" → "feature/…"
268
+ const name = b.slice(slash + 1);
266
269
  if (!name || name.startsWith("HEAD"))
267
270
  continue;
268
- if (!branches.some((x) => x.name === name)) {
269
- branches.push({ name, ref: b }); // ref = "origin/feature/…" (resolvible sin checkout)
271
+ if (!seenNames.has(name.toUpperCase())) {
272
+ branches.push({ name, ref: b });
273
+ seenNames.add(name.toUpperCase());
270
274
  }
271
275
  }
276
+ // 3. Consultar el remoto directamente con git ls-remote para descubrir
277
+ // ramas que existen en el servidor pero nunca fueron descargadas.
278
+ // Filtramos por el prefijo familia para que sea rápido incluso con
279
+ // repositorios que tienen cientos de ramas.
280
+ const remote = shSafe("git remote").split("\n")[0]?.trim() || "origin";
281
+ const lsRemoteRaw = shSafe(`git ls-remote --heads ${q(remote)} ${q(`refs/heads/${familyPrefix}*`)}`);
282
+ for (const line of lsRemoteRaw.split("\n").map((s) => s.trim()).filter(Boolean)) {
283
+ // Formato: "<sha>\trefs/heads/<name>"
284
+ const tabIdx = line.indexOf("\t");
285
+ if (tabIdx < 0)
286
+ continue;
287
+ const ref = line.slice(tabIdx + 1); // "refs/heads/feature/…"
288
+ const name = ref.replace(/^refs\/heads\//, "");
289
+ if (!name || seenNames.has(name.toUpperCase()))
290
+ continue;
291
+ // Usar "<remote>/<name>" como ref resolvible si el tracking existe,
292
+ // o la ref completa "refs/remotes/<remote>/<name>" como fallback.
293
+ // Para git rev-list y git log podemos usar directamente la ref completa.
294
+ const trackingRef = `refs/remotes/${remote}/${name}`;
295
+ const resolvedRef = shSafe(`git rev-parse --verify ${q(trackingRef)}`).trim()
296
+ ? `${remote}/${name}`
297
+ : ref; // "refs/heads/<name>" solo funciona si fue fetched; caer en ls-remote sha
298
+ // Si no hay tracking local, no podemos resolver commits sin fetch.
299
+ // En ese caso hacemos un fetch puntual de esa única rama antes de usarla.
300
+ if (resolvedRef === ref) {
301
+ // fetch puntual y silencioso de solo esta rama para obtener su historial
302
+ shSafe(`git fetch ${q(remote)} ${q(`refs/heads/${name}:refs/remotes/${remote}/${name}`)} --no-tags`);
303
+ }
304
+ branches.push({ name, ref: `${remote}/${name}` });
305
+ seenNames.add(name.toUpperCase());
306
+ }
272
307
  const candidates = branches.filter(({ name }) => name !== currentBranch &&
273
308
  name.toUpperCase().startsWith(familyPrefix.toUpperCase()));
274
309
  if (candidates.length === 0)
275
310
  return null;
276
311
  // De los candidatos, quedarnos con los que tengan commits adelantados
277
312
  // respecto a la base y ordenarlos por committerdate descendente.
278
- // Si la rama solo existe en el remoto, la ref "origin/feature/…" es
279
- // resolvible directamente por git sin necesidad de checkout local.
280
313
  const withAhead = candidates.filter(({ ref }) => {
281
314
  const count = shSafe(`git rev-list ${q(baseBranch)}..${q(ref)} --count`);
282
315
  return (parseInt(count, 10) || 0) > 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pull-request-split-advisor",
3
- "version": "3.2.6",
3
+ "version": "3.2.7",
4
4
  "description": "CLI that analyses your Git working tree and suggests how to split changes into smaller, reviewable pull requests and commits.",
5
5
  "keywords": [
6
6
  "git",