Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions UI/git_sidebar/gitui.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ class GitUi {
this.workspace = null;
this.state = { mode: 'empty', entries: [], commits: [], message: 'Open a folder to use Git.', messageType: 'info' };
this.listening = false;
this.refreshToken = 0;
this.commitMessage = '';
}

async setWorkspace(workspace) {
this.workspace = workspace || null;
this.gitman.setWorkspace(this.workspace);
if (!this.workspace) {
++this.refreshToken;
this.state = { mode: 'empty', entries: [], commits: [], message: 'Open a folder to use Git.', messageType: 'info' };
this.render();
return;
Expand All @@ -26,7 +29,10 @@ class GitUi {
this.listening = true;
this.containerEl.addEventListener('click', (event) => this.handleClick(event));
this.containerEl.addEventListener('input', (event) => {
if (event.target.id === 'git-commit-message') this.renderActionState();
if (event.target.id === 'git-commit-message') {
this.commitMessage = event.target.value;
this.renderActionState();
}
});
}

Expand All @@ -46,6 +52,7 @@ class GitUi {
this.setMessage(`${this.actionLabel(method)}…`, 'loading');
try {
const output = await this.gitman[method](argument);
if (method === 'commit') this.commitMessage = '';
await this.refresh(`${this.actionLabel(method)} completed${output ? `: ${output}` : '.'}`);
} catch (error) {
this.setMessage(error.message || String(error), 'error');
Expand All @@ -58,15 +65,18 @@ class GitUi {

async refresh(successMessage = '') {
if (!this.workspace) return;
const refreshToken = ++this.refreshToken;
this.state = { ...this.state, mode: 'loading', message: 'Loading repository status…', messageType: 'loading' };
this.render();
try {
const [raw, log] = await Promise.all([
this.gitman.status(),
typeof this.gitman.log === 'function' ? this.gitman.log().then((raw) => ({ raw }), (error) => ({ error: error?.message || String(error) })) : Promise.resolve({ raw: '' })
]);
if (refreshToken !== this.refreshToken) return;
this.state = { mode: 'repository', entries: StatusStub.parse(raw), commits: LogStub.parse(log.raw), historyError: log.error || '', message: successMessage || 'Repository status is up to date.', messageType: successMessage ? 'success' : 'info' };
} catch (error) {
if (refreshToken !== this.refreshToken) return;
if (/not a git repository/i.test(error.message || '')) {
this.state = { mode: 'non-repository', entries: [], commits: [], message: 'This folder is not a Git repository.', messageType: 'info' };
} else {
Expand Down Expand Up @@ -147,7 +157,7 @@ class GitUi {
history.append(item);
}
panel.append(history);
const input = this.create('input', { id: 'git-commit-message', className: 'git-commit-input', type: 'text', placeholder: 'Commit message', disabled: busy || !this.state.entries.length });
const input = this.create('input', { id: 'git-commit-message', className: 'git-commit-input', type: 'text', placeholder: 'Commit message', value: this.commitMessage, disabled: busy || !this.state.entries.length });
input.setAttribute('aria-label', 'Commit message');
panel.append(input, this.button('Commit', 'commit', true));
const list = this.create('ul', { className: 'git-status-list' });
Expand Down
57 changes: 56 additions & 1 deletion UI/git_sidebar/gitui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class FakeElement {
append(...children) { this.children.push(...children); }
replaceChildren(...children) { this.children = children; }
setAttribute(name, value) { this[name] = value; }
addEventListener() {}
addEventListener(type, listener) { this.listeners ||= {}; this.listeners[type] = listener; }
querySelector(selector) {
const matches = (element) => selector.startsWith('#') ? element.id === selector.slice(1) : selector.startsWith('.') ? element.className?.split(' ').includes(selector.slice(1)) : selector === '[data-git-action="commit"]' && element.dataset.gitAction === 'commit';
const visit = (element) => matches(element) ? element : element.children.map(visit).find(Boolean);
Expand Down Expand Up @@ -127,3 +127,58 @@ test('keeps status available on history failure and clears the error after recov
assert.match(container.textContent, /No commits yet/);
assert.doesNotMatch(container.textContent, /Unable to load commit history/);
});


test('only the newest refresh can apply success or failure, even after changing or clearing workspaces', async () => {
for (const staleFails of [false, true]) {
for (const nextWorkspace of ['/other', null]) {
let finish;
const pending = new Promise((resolve, reject) => { finish = () => staleFails ? reject(new Error('stale failure')) : resolve(' M stale.js'); });
const gitman = { setWorkspace() {}, status: () => pending };
const ui = new GitUi(createContainer(), { gitman });
const oldRefresh = ui.setWorkspace('/workspace');
gitman.status = async () => ' M current.js';
await ui.setWorkspace(nextWorkspace);
const currentState = ui.state;
finish();
await oldRefresh;
assert.equal(ui.state, currentState);
assert.equal(ui.state.mode, nextWorkspace ? 'repository' : 'empty');
}
}
const pending = deferred();
const gitman = { setWorkspace() {}, status: () => pending.promise };
const ui = new GitUi(createContainer(), { gitman });
const oldRefresh = ui.setWorkspace('/workspace');
gitman.status = async () => ' M newest.js';
await ui.refresh();
pending.resolve(' M stale.js');
await oldRefresh;
assert.match(ui.containerEl.textContent, /newest.js/);
assert.doesNotMatch(ui.containerEl.textContent, /stale.js/);
});

test('preserves commit input through loading and failure and clears it after a successful commit', async () => {
const container = createContainer();
const pending = deferred();
const gitman = { setWorkspace() {}, status: async () => ' M file.js', commit: () => pending.promise };
const ui = new GitUi(container, { gitman });
ui.initListeners();
await ui.setWorkspace('/workspace');
const input = container.querySelector('#git-commit-message');
input.value = 'Keep this draft';
container.listeners.input({ target: input });
const action = ui.runAction('commit', input.value);
assert.equal(container.querySelector('#git-commit-message').value, 'Keep this draft');
pending.resolve('');
await action;
assert.equal(ui.commitMessage, '');
assert.equal(container.querySelector('#git-commit-message').value, '');
input.value = 'Retry this draft';
container.listeners.input({ target: input });
gitman.commit = async () => { throw new Error('commit rejected'); };
await ui.runAction('commit', input.value);
await ui.refresh();
assert.equal(ui.commitMessage, 'Retry this draft');
assert.equal(container.querySelector('#git-commit-message').value, 'Retry this draft');
});
1 change: 1 addition & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ <h1>SATISCODE</h1>
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle Theme">
🌙 Dark Mode
</button>
<a href="webIDE/index.html" class="nav-btn">Web IDE</a>
<a href="pages/about.html" class="nav-btn">About</a>
<a href="pages/versions.html" class="nav-btn">Versions</a>
<a href="pages/contribute.html" class="nav-btn">Contributing</a>
Expand Down
1 change: 1 addition & 0 deletions docs/pages/about.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h1>SATISCODE</h1>
🌙 Dark Mode
</button>
<a href="../index.html" class="nav-btn">Home</a>
<a href="../webIDE/index.html" class="nav-btn">Web IDE</a>
<a href="versions.html" class="nav-btn">Versions</a>
<a href="contribute.html" class="nav-btn">Contributing</a>
<a href="sponsors.html" class="nav-btn">Sponsors</a>
Expand Down
1 change: 1 addition & 0 deletions docs/pages/contribute.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h1>SATISCODE</h1>
🌙 Dark Mode
</button>
<a href="../index.html" class="nav-btn">Home</a>
<a href="../webIDE/index.html" class="nav-btn">Web IDE</a>
<a href="versions.html" class="nav-btn">Versions</a>
<a href="about.html" class="nav-btn">About</a>
<a href="sponsors.html" class="nav-btn">Sponsors</a>
Expand Down
1 change: 1 addition & 0 deletions docs/pages/sponsors.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h1>SATISCODE</h1>
🌙 Dark Mode
</button>
<a href="../index.html" class="nav-btn">Home</a>
<a href="../webIDE/index.html" class="nav-btn">Web IDE</a>
<a href="versions.html" class="nav-btn">Versions</a>
<a href="about.html" class="nav-btn">About</a>
<a href="contribute.html" class="nav-btn">Contributing</a>
Expand Down
1 change: 1 addition & 0 deletions docs/pages/versions.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h1>SATISCODE</h1>
🌙 Dark Mode
</button>
<a href="../index.html" class="nav-btn">Home</a>
<a href="../webIDE/index.html" class="nav-btn">Web IDE</a>
<a href="about.html" class="nav-btn">About</a>
<a href="contribute.html" class="nav-btn">Contributing</a>
<a href="sponsors.html" class="nav-btn">Sponsors</a>
Expand Down
Loading
Loading