Merge branch 'master' into prefer-rebase-when-merging-upstream-into-branch

This commit is contained in:
Kiril Videlov 2024-02-29 13:34:54 +01:00 committed by GitHub
commit 87b0f29754
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 139 additions and 126 deletions

View File

@ -14,15 +14,11 @@
//! Otherwise, neither the length prefix imposed by `(de)serialize_bytes()` nor the
//! terrible compaction and optimization of `(de)serialize_tuple()` are acceptable.
// FIXME(qix-): There are a ton of identifiers in here that make no sense and
// FIXME(qix-): were copied over from the exploratory data-science-ey code that
// FIXME(qix-): need to be cleaned up. PR welcome!
const BITS: usize = 3;
const SHIFT: usize = 8 - BITS;
const SIG_ENTRIES: usize = (1 << BITS) * (1 << BITS);
const SIG_BYTES: usize = SIG_ENTRIES * ::core::mem::size_of::<SigBucket>();
const TOTAL_BYTES: usize = SIG_BYTES + 4 + 1; // we encode a 4-byte length at the beginning, along with a version byte
const FINGERPRINT_ENTRIES: usize = (1 << BITS) * (1 << BITS);
const FINGERPRINT_BYTES: usize = FINGERPRINT_ENTRIES * ::core::mem::size_of::<SigBucket>();
const TOTAL_BYTES: usize = 1 + 4 + FINGERPRINT_BYTES; // we encode a version byte and a 4-byte length at the beginning
// NOTE: This is not efficient if `SigBucket` is 1 byte (u8).
// NOTE: If `SigBucket` is changed to a u8, then the implementation
@ -80,42 +76,44 @@ impl Signature {
/// about the signature or the original file contents.
///
/// Do not use for any security-related purposes.
pub fn score_str<S: AsRef<str>>(&self, s: S) -> f64 {
pub fn score_str<S: AsRef<str>>(&self, other: S) -> f64 {
if self.0[0] != 0 {
panic!("unsupported signature version");
}
let original_length = u32::from_le_bytes(self.0[1..5].try_into().unwrap());
let s = s.as_ref();
let s_s: String = s.chars().filter(|&x| !char::is_whitespace(x)).collect();
let s = s_s.as_bytes();
if original_length < 2 || s.len() < 2 {
let original_length = u32::from_le_bytes(self.0[1..5].try_into().expect("invalid length"));
if original_length < 2 {
return 0.0;
}
let mut intersection_size = 0usize;
let other = other.as_ref();
let other_string: String = other.chars().filter(|&x| !char::is_whitespace(x)).collect();
let other = other_string.as_bytes();
let mut wb = self.bucket_iter().collect::<Vec<_>>();
if other.len() < 2 {
return 0.0;
}
for (b1, b2) in bigrams(s) {
let b1 = b1 >> SHIFT;
let b2 = b2 >> SHIFT;
let ix = ((b1 as usize) << BITS) | (b2 as usize);
if wb[ix] > 0 {
wb[ix] = wb[ix].saturating_sub(1);
intersection_size += 1;
let mut matching_bigrams: usize = 0;
let mut self_buckets = self.bucket_iter().collect::<Vec<_>>();
for (left, right) in bigrams(other) {
let left = left >> SHIFT;
let right = right >> SHIFT;
let index = ((left as usize) << BITS) | (right as usize);
if self_buckets[index] > 0 {
self_buckets[index] = self_buckets[index] - 1;
matching_bigrams += 1;
}
}
(2 * intersection_size) as f64 / (original_length as usize + s.len() - 2) as f64
(2 * matching_bigrams) as f64 / (original_length as usize + other.len() - 2) as f64
}
fn bucket_iter(&self) -> impl Iterator<Item = SigBucket> + '_ {
unsafe {
self.0[(TOTAL_BYTES - SIG_BYTES)..]
self.0[(TOTAL_BYTES - FINGERPRINT_BYTES)..]
.as_chunks_unchecked::<{ ::core::mem::size_of::<SigBucket>() }>()
.iter()
.map(|ch: &[u8; ::core::mem::size_of::<SigBucket>()]| SigBucket::from_le_bytes(*ch))
@ -125,45 +123,50 @@ impl Signature {
impl<S: AsRef<str>> From<S> for Signature {
#[inline]
fn from(s: S) -> Self {
let s = s.as_ref();
fn from(source: S) -> Self {
let source = source.as_ref();
let source_string: String = source
.chars()
.filter(|&x| !char::is_whitespace(x))
.collect();
let source = source_string.as_bytes();
let a_s: String = s.chars().filter(|&x| !char::is_whitespace(x)).collect();
let a = a_s.as_bytes();
let a_len: u32 = a
let source_len: u32 = source
.len()
.try_into()
.expect("strings with a byte-length above u32::MAX are not supported");
let mut a_res = [0; TOTAL_BYTES];
a_res[0] = 0; // version byte
a_res[1..5].copy_from_slice(&a_len.to_le_bytes()); // length
let mut bytes = [0; TOTAL_BYTES];
bytes[0] = 0; // version byte (0)
bytes[1..5].copy_from_slice(&source_len.to_le_bytes()); // next 4 bytes are the length
if a_len >= 2 {
let mut a_bigrams = [0 as SigBucket; SIG_ENTRIES];
if source_len >= 2 {
let mut buckets = [0 as SigBucket; FINGERPRINT_ENTRIES];
for (b1, b2) in bigrams(a) {
let b1 = b1 >> SHIFT;
let b2 = b2 >> SHIFT;
let encoded_bigram = ((b1 as usize) << BITS) | (b2 as usize);
a_bigrams[encoded_bigram] = a_bigrams[encoded_bigram].saturating_add(1);
for (left, right) in bigrams(source) {
let left = left >> SHIFT;
let right = right >> SHIFT;
let index = ((left as usize) << BITS) | (right as usize);
buckets[index] = buckets[index].saturating_add(1);
}
// NOTE: This is not efficient if `SigBucket` is 1 byte (u8).
let mut offset = TOTAL_BYTES - SIG_BYTES;
for bucket in a_bigrams {
let mut offset = TOTAL_BYTES - FINGERPRINT_BYTES;
for bucket in buckets {
let start = offset;
let end = start + ::core::mem::size_of::<SigBucket>();
a_res[start..end].copy_from_slice(&bucket.to_le_bytes());
bytes[start..end].copy_from_slice(&bucket.to_le_bytes());
offset = end;
}
}
Self(a_res)
Self(bytes)
}
}
/// Copies the passed bytes twice and zips them together with a one-byte offset.
/// This produces an iterator of the bigrams (pairs of consecutive bytes) in the input.
/// For example, the bigrams of 1, 2, 3, 4, 5 would be (1, 2), (2, 3), (3, 4), (4, 5).
#[inline]
fn bigrams(s: &[u8]) -> impl Iterator<Item = (u8, u8)> + '_ {
s.iter().copied().zip(s.iter().skip(1).copied())
@ -173,61 +176,47 @@ fn bigrams(s: &[u8]) -> impl Iterator<Item = (u8, u8)> + '_ {
mod tests {
use super::*;
#[test]
fn score_signature() {
let sig = Signature::from("hello world");
macro_rules! assert_score {
($s:expr, $e:expr) => {
if (sig.score_str($s) - $e).abs() >= 0.1 {
($sig:ident, $s:expr, $e:expr) => {
let score = $sig.score_str($s);
if (score - $e).abs() >= 0.1 {
panic!(
"expected score of {} for string {:?}, got {}",
$e,
$s,
sig.score_str($s)
$e, $s, score
);
}
};
}
#[test]
fn score_signature() {
let sig = Signature::from("hello world");
// NOTE: The scores here are not exact, but are close enough
// to be useful for testing purposes, hence why some have the same
// "score" but different strings.
assert_score!("hello world", 1.0);
assert_score!("hello world!", 0.95);
assert_score!("hello world!!", 0.9);
assert_score!("hello world!!!", 0.85);
assert_score!("hello world!!!!", 0.8);
assert_score!("hello world!!!!!", 0.75);
assert_score!("hello world!!!!!!", 0.7);
assert_score!("hello world!!!!!!!", 0.65);
assert_score!("hello world!!!!!!!!", 0.62);
assert_score!("hello world!!!!!!!!!", 0.6);
assert_score!("hello world!!!!!!!!!!", 0.55);
assert_score!(sig, "hello world", 1.0);
assert_score!(sig, "hello world!", 0.95);
assert_score!(sig, "hello world!!", 0.9);
assert_score!(sig, "hello world!!!", 0.85);
assert_score!(sig, "hello world!!!!", 0.8);
assert_score!(sig, "hello world!!!!!", 0.75);
assert_score!(sig, "hello world!!!!!!", 0.7);
assert_score!(sig, "hello world!!!!!!!", 0.65);
assert_score!(sig, "hello world!!!!!!!!", 0.62);
assert_score!(sig, "hello world!!!!!!!!!", 0.6);
assert_score!(sig, "hello world!!!!!!!!!!", 0.55);
}
#[test]
fn score_ignores_whitespace() {
let sig = Signature::from("hello world");
macro_rules! assert_score {
($s:expr, $e:expr) => {
if (sig.score_str($s) - $e).abs() >= 0.1 {
panic!(
"expected score of {} for string {:?}, got {}",
$e,
$s,
sig.score_str($s)
);
}
};
}
assert_score!("hello world", 1.0);
assert_score!("hello world ", 1.0);
assert_score!("hello\nworld ", 1.0);
assert_score!("hello\n\tworld ", 1.0);
assert_score!("\t\t hel lo\n\two rld \t\t", 1.0);
assert_score!(sig, "hello world", 1.0);
assert_score!(sig, "hello world ", 1.0);
assert_score!(sig, "hello\nworld ", 1.0);
assert_score!(sig, "hello\n\tworld ", 1.0);
assert_score!(sig, "\t\t hel lo\n\two rld \t\t", 1.0);
}
const TEXT1: &str = include_str!("../fixture/text1.txt");

View File

@ -1,4 +1,5 @@
import { showToast } from '$lib/notifications/toasts';
import { relaunch } from '@tauri-apps/api/process';
import {
checkUpdate,
installUpdate,
@ -8,19 +9,20 @@ import {
} from '@tauri-apps/api/updater';
import posthog from 'posthog-js';
import {
BehaviorSubject,
switchMap,
Observable,
from,
map,
shareReplay,
interval,
timeout,
catchError,
of,
tap,
map,
from,
timeout,
interval,
switchMap,
shareReplay,
catchError,
startWith,
combineLatestWith,
tap
distinctUntilChanged,
Observable,
BehaviorSubject
} from 'rxjs';
// TOOD: Investigate why 'DOWNLOADED' is not in the type provided by Tauri.
@ -44,7 +46,10 @@ export class UpdaterService {
constructor() {
onUpdaterEvent((status) => {
const err = status.error;
if (err) showErrorToast(err);
if (err) {
showErrorToast(err);
posthog.capture('App Update Status Error', { error: err });
}
this.status$.next(status.status);
}).then((unlistenFn) => (this.unlistenFn = unlistenFn));
@ -58,10 +63,12 @@ export class UpdaterService {
map((update: UpdateResult | undefined) => {
if (update?.shouldUpdate) return update.manifest;
}),
// We don't need the stream to emit if the result is the same version
distinctUntilChanged((prev, curr) => prev?.version == curr?.version),
// Hide offline/timeout errors since no app ever notifies you about this
catchError((err) => {
if (!isOffline(err) && !isTimeoutError(err)) {
posthog.capture('Updater Check Error', err);
posthog.capture('App Update Check Error', { error: err });
showErrorToast(err);
console.log(err);
}
@ -83,15 +90,19 @@ export class UpdaterService {
// });
}
async install() {
async installUpdate() {
try {
await installUpdate();
posthog.capture('App Update Successful');
} catch (e: any) {
} catch (err: any) {
// We expect toast to be shown by error handling in `onUpdaterEvent`
posthog.capture('App Update Failed', e);
posthog.capture('App Update Install Error', { error: err });
}
}
relaunchApp() {
relaunch();
}
}
function isOffline(err: any): boolean {
@ -118,5 +129,4 @@ function showErrorToast(err: any) {
`,
style: 'error'
});
posthog.capture('Updater Status Error', err);
}

View File

@ -2,18 +2,13 @@
import Button from './Button.svelte';
import IconButton from './IconButton.svelte';
import { showToast } from '$lib/notifications/toasts';
import { relaunch } from '@tauri-apps/api/process';
import { installUpdate } from '@tauri-apps/api/updater';
import { distinctUntilChanged, tap } from 'rxjs';
import { tap } from 'rxjs';
import { fade } from 'svelte/transition';
import type { UpdaterService } from '$lib/backend/updater';
export let updaterService: UpdaterService;
// Extrend update stream to allow dismissing updater by version
$: update$ = updaterService.update$.pipe(
// Only run operators after this one once per version
distinctUntilChanged((prev, curr) => prev?.version == curr?.version),
// Reset dismissed boolean when a new version becomes available
tap(() => (dismissed = false))
);
@ -99,8 +94,10 @@
New version available
{:else if $update$.status == 'PENDING'}
Downloading update...
{:else if $update$.status == 'DONE'}
{:else if $update$.status == 'DOWNLOADED'}
Installing update...
{:else if $update$.status == 'DONE'}
Install complete
{:else if $update$.status == 'ERROR'}
Error occurred...
{/if}
@ -128,11 +125,18 @@
{#if !$update$.status}
<div class="cta-btn" transition:fade={{ duration: 100 }}>
<Button wide on:click={() => installUpdate()}>Download {$update$.version}</Button>
<Button
wide
on:click={async () => {
await updaterService.installUpdate();
}}
>
Download {$update$.version}
</Button>
</div>
{:else if $update$.status == 'DONE'}
<div class="cta-btn" transition:fade={{ duration: 100 }}>
<Button wide on:click={() => relaunch()}>Restart to update</Button>
<Button wide on:click={() => updaterService.relaunchApp()}>Restart</Button>
</div>
{/if}
</div>

View File

@ -58,6 +58,7 @@
};
function commit() {
if (!commitMessage) return;
isCommitting = true;
branchController
.commitBranch(branch.id, commitMessage, $selectedOwnership.toString(), $runCommitHooks)
@ -72,7 +73,7 @@
return invoke<string>('git_get_global_config', params);
}
let isGeneratingCommigMessage = false;
let isGeneratingCommitMessage = false;
async function generateCommitMessage(files: LocalFile[]) {
const diff = files
.map((f) => f.hunks.filter((h) => $selectedOwnership.containsHunk(f.id, h.id)))
@ -91,7 +92,7 @@
if (branch.name.toLowerCase().includes('virtual branch')) {
dispatch('action', 'generate-branch-name');
}
isGeneratingCommigMessage = true;
isGeneratingCommitMessage = true;
cloud.summarize
.commit(user.access_token, {
diff,
@ -114,7 +115,7 @@
toasts.error('Failed to generate commit message');
})
.finally(() => {
isGeneratingCommigMessage = false;
isGeneratingCommitMessage = false;
});
}
const commitGenerationExtraConcise = projectCommitGenerationExtraConcise(projectId);
@ -134,10 +135,15 @@
on:input={useAutoHeight}
on:focus={useAutoHeight}
on:change={() => currentCommitMessage.set(commitMessage)}
on:keydown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
commit();
}
}}
spellcheck={false}
class="text-input text-base-body-13 commit-box__textarea"
rows="1"
disabled={isGeneratingCommigMessage}
disabled={isGeneratingCommitMessage}
placeholder="Your commit message here"
/>
@ -152,7 +158,7 @@
icon="ai-small"
color="neutral"
disabled={!$aiGenEnabled || !user}
loading={isGeneratingCommigMessage}
loading={isGeneratingCommitMessage}
on:click={() => generateCommitMessage(branch.files)}
>
Generate message
@ -200,9 +206,7 @@
id="commit-to-branch"
on:click={() => {
if ($expanded) {
if (commitMessage) {
commit();
}
} else {
$expanded = true;
}

View File

@ -184,7 +184,7 @@
<svelte:fragment slot="title">Use locally generated SSH key</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton bind:group={selectedOption} value="generated" />
<RadioButton bind:group={selectedOption} value="generated" on:input={setGeneratedKey} />
</svelte:fragment>
<svelte:fragment slot="body">
@ -238,7 +238,11 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton bind:group={selectedOption} value="gitCredentialsHelper" />
<RadioButton
bind:group={selectedOption}
value="gitCredentialsHelper"
on:input={setGitCredentialsHelperKey}
/>
</svelte:fragment>
</ClickableCard>
</fieldset>

View File

@ -10,6 +10,7 @@
<input
on:click|stopPropagation
on:change
on:input
type="radio"
class="radio"
class:small

View File

@ -1,8 +1,8 @@
<script lang="ts">
import '../styles/main.postcss';
import AppUpdater from '$lib/components/AppUpdater.svelte';
import ShareIssueModal from '$lib/components/ShareIssueModal.svelte';
import UpdateButton from '$lib/components/UpdateButton.svelte';
import ToastController from '$lib/notifications/ToastController.svelte';
import { SETTINGS_CONTEXT, loadUserSettings } from '$lib/settings/userSettings';
import * as events from '$lib/utils/events';
@ -52,4 +52,4 @@
<Toaster />
<ShareIssueModal bind:this={shareIssueModal} user={$user$} {cloud} />
<ToastController />
<UpdateButton {updaterService} />
<AppUpdater {updaterService} />

View File

@ -71,6 +71,7 @@
name: newName,
picture: picture
});
updatedUser.github_access_token = $user$?.github_access_token; // prevent overwriting with null
userService.setUser(updatedUser);
toasts.success('Profile updated');
} catch (e) {