@osdlc/repo-cleaner 0.19.1 → 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,11 @@
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
+
3
9
  ## 0.19.1
4
10
 
5
11
  ### Patch 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
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
  }
@@ -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.1";
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.1';
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.1",
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",
@@ -49,9 +49,9 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "zod": "^4.6.6",
52
+ "@osdlc/cli-tool": "^0.17.0",
52
53
  "@osdlc/command-seam": "^0.14.0",
53
54
  "@osdlc/path-guard": "^0.3.1",
54
- "@osdlc/cli-tool": "^0.17.0",
55
55
  "@osdlc/typed-gh": "^0.16.1",
56
56
  "@osdlc/ux": "^0.1.1",
57
57
  "@osdlc/typed-git": "^0.13.0"
@@ -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",