git-watchtower 2.1.7 → 2.1.9

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.
@@ -1602,6 +1602,43 @@ async function undoLastSwitch() {
1602
1602
  }
1603
1603
 
1604
1604
  const lastSwitch = currentHistory[0];
1605
+
1606
+ // Detached HEAD restore: switchHistory captured the previous state as the
1607
+ // synthetic "HEAD@<hash>" name produced by getCurrentBranch(). sanitizeBranchName
1608
+ // rejects '@', so round-tripping through switchToBranch fails with "Invalid
1609
+ // Branch Name". Detect the detached form and `git checkout <hash>` directly.
1610
+ const detachedMatch = /^HEAD@([0-9a-f]{4,40})$/i.exec(lastSwitch.from);
1611
+ if (detachedMatch) {
1612
+ const hash = detachedMatch[1];
1613
+ addLog(`Undoing: restoring detached HEAD at ${hash}`, 'update');
1614
+
1615
+ if (await hasUncommittedChanges()) {
1616
+ addLog('Cannot undo: uncommitted changes in working directory', 'error');
1617
+ pendingDirtyOperation = { type: 'switch', branch: lastSwitch.from };
1618
+ showStashConfirm(`undo to detached HEAD ${hash}`);
1619
+ return { success: false, reason: 'dirty' };
1620
+ }
1621
+
1622
+ try {
1623
+ await execGit(['checkout', hash], { cwd: PROJECT_ROOT });
1624
+ store.setState({
1625
+ currentBranch: lastSwitch.from,
1626
+ isDetachedHead: true,
1627
+ switchHistory: store.get('switchHistory').slice(1),
1628
+ });
1629
+ addLog(`Undone: detached HEAD at ${hash}`, 'success');
1630
+ branchSwitchCount++;
1631
+ pendingDirtyOperation = null;
1632
+ notifyClients();
1633
+ return { success: true };
1634
+ } catch (e) {
1635
+ const errMsg = e.stderr || e.message || String(e);
1636
+ addLog(`Undo failed: ${errMsg}`, 'error');
1637
+ showErrorToast('Undo Failed', truncate(errMsg, 100), 'Commit may have been garbage-collected');
1638
+ return { success: false };
1639
+ }
1640
+ }
1641
+
1605
1642
  addLog(`Undoing: going back to ${lastSwitch.from}`, 'update');
1606
1643
 
1607
1644
  const result = await switchToBranch(lastSwitch.from, false);
@@ -3125,12 +3162,12 @@ async function handleWebAction(action, payload) {
3125
3162
  sendResult(true, 'Fetch complete');
3126
3163
  break;
3127
3164
  case 'undo': {
3128
- const last = store.getLastSwitch();
3129
- if (last) {
3130
- await switchToBranch(last.from);
3131
- store.popHistory();
3132
- await pollGitChanges();
3133
- sendResult(true, `Switched back to ${last.from}`);
3165
+ // Delegate to undoLastSwitch so detached-HEAD restoration and
3166
+ // history popping stay consistent with the TUI 'u' keybinding.
3167
+ const result = await undoLastSwitch();
3168
+ await pollGitChanges();
3169
+ if (result.success) {
3170
+ sendResult(true, `Undone last switch`);
3134
3171
  } else {
3135
3172
  sendResult(false, 'No switch to undo');
3136
3173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
package/src/git/branch.js CHANGED
@@ -97,7 +97,12 @@ async function getAllBranches(options = {}) {
97
97
  }
98
98
 
99
99
  const branchList = [];
100
- const seenBranches = new Set();
100
+ // O(1) lookup by name. The Map stores the same object references
101
+ // pushed into branchList, so mutating an entry through the Map (when
102
+ // a remote ref matches a local ref) updates the array entry too.
103
+ // Replaces the previous `branchList.find()` per-remote-ref scan,
104
+ // which was O(n²) and noticeable on large monorepos.
105
+ const branchByName = new Map();
101
106
 
102
107
  // Get local branches
103
108
  // Use \x1f (Unit Separator) as delimiter since | can appear in commit subjects
@@ -111,9 +116,8 @@ async function getAllBranches(options = {}) {
111
116
  for (const line of localResult.stdout.split('\n').filter(Boolean)) {
112
117
  const [name, dateStr, commit, ...subjectParts] = line.split(delimiter);
113
118
  const subject = subjectParts.join(delimiter);
114
- if (!seenBranches.has(name) && isValidBranchName(name)) {
115
- seenBranches.add(name);
116
- branchList.push({
119
+ if (!branchByName.has(name) && isValidBranchName(name)) {
120
+ const branch = {
117
121
  name,
118
122
  commit,
119
123
  subject: subject || '',
@@ -121,7 +125,9 @@ async function getAllBranches(options = {}) {
121
125
  isLocal: true,
122
126
  hasRemote: false,
123
127
  hasUpdates: false,
124
- });
128
+ };
129
+ branchByName.set(name, branch);
130
+ branchList.push(branch);
125
131
  }
126
132
  }
127
133
  }
@@ -142,7 +148,7 @@ async function getAllBranches(options = {}) {
142
148
  if (name === 'HEAD') continue;
143
149
  if (!isValidBranchName(name)) continue;
144
150
 
145
- const existing = /** @type {Branch|undefined} */ (branchList.find((b) => b.name === name));
151
+ const existing = /** @type {Branch|undefined} */ (branchByName.get(name));
146
152
  if (existing) {
147
153
  existing.hasRemote = true;
148
154
  existing.remoteCommit = commit;
@@ -154,9 +160,8 @@ async function getAllBranches(options = {}) {
154
160
  existing.date = new Date(dateStr);
155
161
  existing.subject = subject || existing.subject;
156
162
  }
157
- } else if (!seenBranches.has(name)) {
158
- seenBranches.add(name);
159
- branchList.push({
163
+ } else {
164
+ const branch = {
160
165
  name,
161
166
  commit,
162
167
  subject: subject || '',
@@ -164,7 +169,9 @@ async function getAllBranches(options = {}) {
164
169
  isLocal: false,
165
170
  hasRemote: true,
166
171
  hasUpdates: false,
167
- });
172
+ };
173
+ branchByName.set(name, branch);
174
+ branchList.push(branch);
168
175
  }
169
176
  }
170
177
  }