@senomas/pi-git-hat 0.2.2 → 0.2.3
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/git-hat.ts +106 -1
- package/package.json +1 -1
package/git-hat.ts
CHANGED
|
@@ -1141,9 +1141,114 @@ export default function (pi: ExtensionAPI) {
|
|
|
1141
1141
|
},
|
|
1142
1142
|
});
|
|
1143
1143
|
|
|
1144
|
+
// -- /hatr command: rebase onto most recent role-matched branch --
|
|
1145
|
+
|
|
1146
|
+
pi.registerCommand("hatr", {
|
|
1147
|
+
description: "Rebase current branch onto the most recent role-matched branch",
|
|
1148
|
+
handler: async (_args, ctx) => {
|
|
1149
|
+
// Edge case: detached HEAD or non-git directory
|
|
1150
|
+
const branch = await detectBranch();
|
|
1151
|
+
if (!branch) {
|
|
1152
|
+
ctx.ui.notify("Not on a branch (detached HEAD or not a git repo). Aborting.", "warning");
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const currentBranch = branch;
|
|
1156
|
+
|
|
1157
|
+
// List all local branches
|
|
1158
|
+
let allBranches: string[] = [];
|
|
1159
|
+
try {
|
|
1160
|
+
const result = await pi.exec("git", ["branch", "--format", "%(refname:short)"]);
|
|
1161
|
+
allBranches = result.stdout.trim().split("\n").filter(Boolean);
|
|
1162
|
+
} catch {
|
|
1163
|
+
ctx.ui.notify("Failed to list git branches.", "error");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Filter to role-matched branches, excluding current
|
|
1168
|
+
const candidates: string[] = [];
|
|
1169
|
+
for (const b of allBranches) {
|
|
1170
|
+
if (b === currentBranch) continue;
|
|
1171
|
+
if (detectRole(b, config)) candidates.push(b);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Edge case: no other role-matched branches
|
|
1175
|
+
if (candidates.length === 0) {
|
|
1176
|
+
ctx.ui.notify("No other role-matched branches to rebase onto.", "info");
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Get latest commit info for each candidate
|
|
1181
|
+
interface CandidateInfo {
|
|
1182
|
+
branch: string;
|
|
1183
|
+
timestamp: number;
|
|
1184
|
+
abbrev: string;
|
|
1185
|
+
subject: string;
|
|
1186
|
+
}
|
|
1187
|
+
const infos: CandidateInfo[] = [];
|
|
1188
|
+
for (const b of candidates) {
|
|
1189
|
+
try {
|
|
1190
|
+
const result = await pi.exec("git", [
|
|
1191
|
+
"log", "--format=%ct %h %s", "-1", b,
|
|
1192
|
+
]);
|
|
1193
|
+
const line = result.stdout.trim();
|
|
1194
|
+
if (!line) continue;
|
|
1195
|
+
const space1 = line.indexOf(" ");
|
|
1196
|
+
const space2 = line.indexOf(" ", space1 + 1);
|
|
1197
|
+
if (space1 === -1 || space2 === -1) continue;
|
|
1198
|
+
const timestamp = parseInt(line.slice(0, space1), 10);
|
|
1199
|
+
const abbrev = line.slice(space1 + 1, space2);
|
|
1200
|
+
const subject = line.slice(space2 + 1);
|
|
1201
|
+
infos.push({ branch: b, timestamp, abbrev, subject });
|
|
1202
|
+
} catch {
|
|
1203
|
+
// skip branches we can't read
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (infos.length === 0) {
|
|
1208
|
+
ctx.ui.notify("Could not determine latest commits for candidates.", "error");
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Pick the most recent
|
|
1213
|
+
infos.sort((a, b) => b.timestamp - a.timestamp);
|
|
1214
|
+
const target = infos[0];
|
|
1215
|
+
|
|
1216
|
+
// Show and confirm
|
|
1217
|
+
const confirmed = await ctx.ui.confirm(
|
|
1218
|
+
`Rebase ${currentBranch} onto ${target.branch}? (latest commit: ${target.abbrev} ${target.subject})`,
|
|
1219
|
+
);
|
|
1220
|
+
if (!confirmed) {
|
|
1221
|
+
ctx.ui.notify("Rebase cancelled.", "info");
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Run the rebase
|
|
1226
|
+
try {
|
|
1227
|
+
const result = await pi.exec("git", ["rebase", target.branch]);
|
|
1228
|
+
if (result.code === 0) {
|
|
1229
|
+
ctx.ui.notify(
|
|
1230
|
+
`Rebase onto ${target.branch} succeeded. Updated commit tree:`,
|
|
1231
|
+
"info",
|
|
1232
|
+
);
|
|
1233
|
+
await showGitLog(ctx, 10);
|
|
1234
|
+
} else {
|
|
1235
|
+
ctx.ui.notify(
|
|
1236
|
+
`Rebase onto ${target.branch} failed:\n${result.stderr}\n\nResolve conflicts manually, then run \`git rebase --continue\`.`,
|
|
1237
|
+
"error",
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
} catch (e) {
|
|
1241
|
+
ctx.ui.notify(
|
|
1242
|
+
`Rebase onto ${target.branch} failed: ${(e as Error).message}\n\nResolve conflicts manually, then run \`git rebase --continue\`.`,
|
|
1243
|
+
"error",
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
},
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1144
1249
|
// -- Session lifecycle ----------------------------------------
|
|
1145
1250
|
|
|
1146
|
-
pi.on("session_start", async (event, ctx) => {
|
|
1251
|
+
pi.on("session_start", async (event, ctx) => {"}]}
|
|
1147
1252
|
cwdAbsolute = ctx.cwd;
|
|
1148
1253
|
|
|
1149
1254
|
// Seed bundled roles/ files to project .pi/ before loading config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senomas/pi-git-hat",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Pi extension for role-based Git branch workflows — wear different hats by switching branches",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package", "git", "workflow", "branching", "roles"],
|