@osdlc/repo-cleaner 0.19.0 → 0.20.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @osdlc/repo-cleaner
2
2
 
3
+ ## 0.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9171850: Run with no target from a subdirectory of a checkout, `repo-cleaner` now sweeps that checkout instead of finding nothing and exiting 10. It widens only when the search below finds no checkout and the checkout around tracks files in the directory, so `~/Developer` under a home directory kept in git still sweeps the checkouts below it. A subdirectory named as a target is read as written, as before.
8
+
9
+ ## 0.19.1
10
+
11
+ ### Patch Changes
12
+
13
+ - bfac1ae: The note that suggests `--provider none` now names what the provider lists ("if this remote has no PRs", or "no merge requests" for GitLab) instead of "if this remote has no forge", and the README drops "forge" the same way.
14
+
3
15
  ## 0.19.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -115,8 +115,15 @@ A target is what to sweep:
115
115
 
116
116
  No target is the same as naming the current directory: at a checkout's root
117
117
  it sweeps that repository, and in a directory of checkouts it sweeps each
118
- one. In a subdirectory of a checkout it searches below that subdirectory,
119
- like any other directory, so name the checkout instead.
118
+ one. In a subdirectory of a checkout, such as `src/`, it sweeps the checkout
119
+ around it. That happens when the search below finds no checkout and the
120
+ checkout around tracks files in the directory (or git cannot say, in which
121
+ case the report gives git's reason). So a directory of checkouts inside a
122
+ checkout of its own, such as `~/Developer` under a home directory kept in
123
+ git, still sweeps the checkouts below it, or reports that it found none, as
124
+ long as the home repository tracks no file there. A search that finds
125
+ checkouts it cannot read is reported as it is. A subdirectory named as a
126
+ target is read as written and searched.
120
127
  What a target names is read off the disk rather than from how it is written:
121
128
  a path that is itself a checkout names that repository, and a path that is
122
129
  not is searched for the checkouts under it, descending through directories
@@ -597,7 +604,7 @@ a failed fetch costs the whole repository.
597
604
  | What failed | What you get |
598
605
  | --- | --- |
599
606
  | `gh pr list` / `glab api` (the PR listing) | Every row is judged on git evidence. Remote branches with no known PR need `--allow confirm-open-pr` to delete. One note on the repository. |
600
- | `gh` / `glab` not installed or not signed in | The provider is never asked, so this is treated like a failed listing: every row is judged on git evidence, remote branches with no known PR need `--allow confirm-open-pr` to delete, and a note on the repository says why. Pass `--provider none` if the remote has no forge, so there is nothing to check. |
607
+ | `gh` / `glab` not installed or not signed in | The provider is never asked, so this is treated like a failed listing: every row is judged on git evidence, remote branches with no known PR need `--allow confirm-open-pr` to delete, and a note on the repository says why. Pass `--provider none` if the remote has no pull requests, so there is nothing to check. |
601
608
  | `git ls-remote`, after a fetch that worked | Remote branches are read from the refs that fetch just wrote. A note on the repository and on each remote row. |
602
609
  | `git ls-remote`, in `clean`'s re-check of an `--offline` plan | The run is refused before anything runs. The refs on disk are what the re-check is there to replace, and `clean` does not delete on a remote it cannot ask. |
603
610
  | `git fetch` | The repository is an error with no rows. Every row would otherwise be judged against an older default branch than the one you asked for. |
@@ -630,7 +637,7 @@ $ repo-cleaner --remote --offline
630
637
  2 to delete, 0 to review, across 1 repository.
631
638
  ```
632
639
 
633
- When the remote may not be on a forge at all, the note also suggests
640
+ When the remote may have no pull requests at all, the note also suggests
634
641
  `--provider none`: for a host `auto` could only guess was GitHub, or a local
635
642
  path swept with `--provider github`. Under `auto`, a local path gets no
636
643
  provider in the first place.
@@ -656,8 +663,8 @@ status: complete
656
663
  The local branch went; the remote branch stayed. To go on, pick one:
657
664
 
658
665
  - Fix the lookup (install `gh`, sign in, check the remote URL) and run again.
659
- - Pass `--provider none` if this remote has no forge at all, so there is no
660
- pull request to lose.
666
+ - Pass `--provider none` if this remote has no pull requests at all, so there
667
+ is none to lose.
661
668
  - Check for open pull requests yourself, then pass `--allow confirm-open-pr`.
662
669
 
663
670
  The same happens when `gh` is not installed or not signed in. Nothing was
@@ -675,8 +682,8 @@ $ repo-cleaner --remote --offline
675
682
  2 to delete, 0 to review, across 1 repository.
676
683
  ```
677
684
 
678
- `--provider none` is how you say the remote has no forge: then there is no
679
- lookup, no note and no gate.
685
+ `--provider none` is how you say the remote has no pull requests: then there
686
+ is no lookup, no note and no gate.
680
687
 
681
688
  ### `--offline` reads what is already on disk
682
689
 
@@ -707,7 +714,7 @@ $ repo-cleaner --remote --offline --provider none # now
707
714
  `--provider none` to judge with no network at all.
708
715
 
709
716
  The refs on disk can be stale. A remote-tracking ref stays until the next
710
- `git fetch --prune`, so a branch the forge deleted after its pull request
717
+ `git fetch --prune`, so a branch the host deleted after its pull request
711
718
  merged still shows in the report, and a branch someone pushed to since shows
712
719
  its old tip. That is fine for a report, but `clean` deletes on the remote, so
713
720
  before it does, its re-check asks the remote itself with `git ls-remote`. A
@@ -779,7 +786,7 @@ is allowed.
779
786
 
780
787
  A `PrProvider` you write reports a failure by returning `null` or by
781
788
  throwing; either becomes `listError` and a note. Return an empty list only
782
- when the forge answered and there are no pull requests.
789
+ when the provider answered and there are no pull requests.
783
790
 
784
791
  The decision behind this is
785
792
  [D-UVUH](docs/planning/decisions/D-UVUH-failed-lookups.md).
@@ -967,7 +974,7 @@ Not done yet:
967
974
 
968
975
  `git` on `PATH`, and `gh` (or `glab` for GitLab) for pull request evidence.
969
976
  Without it, deleting a remote branch needs `--allow confirm-open-pr`, or
970
- `--provider none` for a remote with no forge (see
977
+ `--provider none` for a remote with no pull requests (see
971
978
  [When a lookup fails](#when-a-lookup-fails)). Authentication is the CLI's
972
979
  own — this package reads no token.
973
980
 
package/dist/reference.js CHANGED
@@ -9,7 +9,8 @@ const TARGETS = `Targets
9
9
  a directory every checkout found under it, three levels deep at most
10
10
  a remote URL its remote branches only, through a clone kept in the cache
11
11
  directory (https://, ssh://, file://, or user@host:path)
12
- No target sweeps the directory you are in, read the same way.`;
12
+ No target sweeps the directory you are in, read the same way; in a subdirectory
13
+ the checkout around it tracks, where the search finds no checkout, that checkout.`;
13
14
  const SWEEP = `Sweep options (the report, plan and clean)
14
15
  --remote Also judge remote branches. Turns on the network: fetches
15
16
  each remote first and asks GitHub or GitLab about pull
package/dist/resolve.d.ts CHANGED
@@ -8,9 +8,10 @@
8
8
  * covers everything under it — including a per-owner or per-client
9
9
  * directory — without listing any of them.
10
10
  *
11
- * A caller that passes no entries gets no repositories. The CLI's bare
12
- * invocation passes the directory it stands in, which resolves like any
13
- * other entry.
11
+ * A caller that passes no entries gets no repositories. `sweep` with no
12
+ * targets passes the directory it stands in, which resolves like any other
13
+ * entry; when that finds nothing in a directory the checkout around it tracks,
14
+ * `sweep` widens it to that checkout.
14
15
  */
15
16
  import { type Git } from '@osdlc/typed-git';
16
17
  import { type ScanStats, type UnresolvedEntry } from './types.ts';
package/dist/resolve.js CHANGED
@@ -8,9 +8,10 @@
8
8
  * covers everything under it — including a per-owner or per-client
9
9
  * directory — without listing any of them.
10
10
  *
11
- * A caller that passes no entries gets no repositories. The CLI's bare
12
- * invocation passes the directory it stands in, which resolves like any
13
- * other entry.
11
+ * A caller that passes no entries gets no repositories. `sweep` with no
12
+ * targets passes the directory it stands in, which resolves like any other
13
+ * entry; when that finds nothing in a directory the checkout around it tracks,
14
+ * `sweep` widens it to that checkout.
14
15
  */
15
16
  import { homedir } from 'node:os';
16
17
  import { isAbsolute, join, resolve as resolvePath } from 'node:path';
package/dist/sweep.d.ts CHANGED
@@ -67,7 +67,9 @@ export declare function sweptWith(targets: readonly string[], opts: Omit<SweepOp
67
67
  /**
68
68
  * Sweep `targets`: checkouts, directories to search for checkouts, or remote
69
69
  * URLs (swept through a clone kept in `cacheDir`, remote branches only). No
70
- * targets means `[cwd]`, resolved the same way.
70
+ * targets means `[cwd]`, resolved the same way, except that a `cwd` where the
71
+ * search finds no checkout, and whose checkout tracks files in it, sweeps
72
+ * that checkout.
71
73
  *
72
74
  * Never throws for a repository it cannot read: that one comes back with
73
75
  * `error` set, because a fan-out must not end on its first bad entry. Throws a
package/dist/sweep.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { existsSync, realpathSync } from 'node:fs';
10
10
  import { isAbsolute, resolve } from 'node:path';
11
- import { Git } from '@osdlc/typed-git';
11
+ import { Git, checkoutRoot } from '@osdlc/typed-git';
12
12
  import { Gh } from '@osdlc/typed-gh';
13
13
  import { HELD_BY_WORKTREE, gatherLocalBranches, gatherRemoteBranches, resolveBranch, } from "./branches.js";
14
14
  import { discoverRepo } from "./discover.js";
@@ -103,10 +103,21 @@ export function sweptWith(targets, opts) {
103
103
  urls,
104
104
  };
105
105
  }
106
+ /**
107
+ * Whether the checkout `dir` is in tracks any file under it. True as well when
108
+ * git cannot say: then widening to that checkout reports why it cannot be
109
+ * read, which says more than "no git checkouts under it".
110
+ */
111
+ function tracksFilesIn(dir, git) {
112
+ const listed = git.at(dir).try.lsFiles({ cached: true });
113
+ return !listed.ok || listed.value.length > 0;
114
+ }
106
115
  /**
107
116
  * Sweep `targets`: checkouts, directories to search for checkouts, or remote
108
117
  * URLs (swept through a clone kept in `cacheDir`, remote branches only). No
109
- * targets means `[cwd]`, resolved the same way.
118
+ * targets means `[cwd]`, resolved the same way, except that a `cwd` where the
119
+ * search finds no checkout, and whose checkout tracks files in it, sweeps
120
+ * that checkout.
110
121
  *
111
122
  * Never throws for a repository it cannot read: that one comes back with
112
123
  * `error` set, because a fan-out must not end on its first bad entry. Throws a
@@ -131,13 +142,26 @@ export function sweep(targets, opts = {}) {
131
142
  }
132
143
  }
133
144
  }
134
- const resolution = entries.length === 0
145
+ const resolveOpts = {
146
+ git: new Git(cwd, { runner: opts.git }),
147
+ baseDir: cwd,
148
+ ...(opts.onWarn === undefined ? {} : { onWarn: opts.onWarn }),
149
+ };
150
+ let resolution = entries.length === 0
135
151
  ? { repos: [], unresolved: [], scan: emptyScan() }
136
- : resolveEntries(entries, {
137
- git: new Git(cwd, { runner: opts.git }),
138
- baseDir: cwd,
139
- ...(opts.onWarn === undefined ? {} : { onWarn: opts.onWarn }),
140
- });
152
+ : resolveEntries(entries, resolveOpts);
153
+ // With no target, a directory the checkout around it tracks files in means
154
+ // that checkout, as it does for git itself. The search comes first, and a
155
+ // directory the checkout tracks nothing in is not part of it: a home
156
+ // directory kept in git does not track ~/Developer, so a search there that
157
+ // finds nothing is reported as it is. So is a search that found checkouts
158
+ // it could not read.
159
+ const around = targets.length === 0 && resolution.repos.length === 0 && resolution.scan.checkoutsFound === 0
160
+ ? checkoutRoot(cwd)
161
+ : null;
162
+ if (around !== null && around !== resolve(cwd) && tracksFilesIn(cwd, resolveOpts.git)) {
163
+ resolution = resolveEntries([around], resolveOpts);
164
+ }
141
165
  if (branch !== undefined && resolution.repos.length > 1) {
142
166
  throw new RepoCleanerError('conflict', `--branch sweeps one repository; ${resolution.repos.length} found under ${entries[0]}`);
143
167
  }
@@ -182,7 +206,7 @@ function providerNote(repo, kinds, choice) {
182
206
  // is a forge: there the cause is access, not the provider.
183
207
  const hint = status.host === null ||
184
208
  (choice === 'auto' && status.name === 'github' && status.host !== 'github.com')
185
- ? ' Pass `--provider none` if this remote has no forge.'
209
+ ? ` Pass \`--provider none\` if this remote has no ${status.noun}s.`
186
210
  : '';
187
211
  if (unchecked.kind === 'list-failed') {
188
212
  return `Listing ${status.noun}s with \`${status.listing}\` failed (${status.listError}); judged on git evidence alone.${hint}`;
@@ -1,2 +1,2 @@
1
1
  /** This package's version, from its package.json when it was built. */
2
- export declare const VERSION = "0.19.0";
2
+ export declare const VERSION = "0.20.0";
@@ -1,3 +1,3 @@
1
1
  // Generated by cli-tool-stamp-version from package.json. Do not edit.
2
2
  /** This package's version, from its package.json when it was built. */
3
- export const VERSION = '0.19.0';
3
+ export const VERSION = '0.20.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osdlc/repo-cleaner",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "What has been merged into the default branch, and what can go away. Inventories local branches, worktrees and remote branches across one repository or many, classifying each with the evidence behind the verdict and the command that would act on it. Plans and applies approved deletes, compare-and-swap.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "sksizer",
@@ -50,9 +50,9 @@
50
50
  "dependencies": {
51
51
  "zod": "^4.6.6",
52
52
  "@osdlc/cli-tool": "^0.17.0",
53
+ "@osdlc/command-seam": "^0.14.0",
53
54
  "@osdlc/path-guard": "^0.3.1",
54
55
  "@osdlc/typed-gh": "^0.16.1",
55
- "@osdlc/command-seam": "^0.14.0",
56
56
  "@osdlc/ux": "^0.1.1",
57
57
  "@osdlc/typed-git": "^0.13.0"
58
58
  },
@@ -63,8 +63,8 @@
63
63
  "prettier": "3.9.6",
64
64
  "typescript": "~6.0.3",
65
65
  "vitest": "^5.0.5",
66
- "@osdlc/prettier-config": "^0.3.1",
67
- "@osdlc/tsconfig": "^0.4.1"
66
+ "@osdlc/tsconfig": "^0.4.1",
67
+ "@osdlc/prettier-config": "^0.3.1"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "cli-tool-stamp-version && tsc -p tsconfig.build.json",