mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-20 16:11:46 +03:00
Merge branch 'master' into prefer-rebase-when-merging-upstream-into-branch
This commit is contained in:
commit
87b0f29754
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -10,6 +10,7 @@
|
||||
<input
|
||||
on:click|stopPropagation
|
||||
on:change
|
||||
on:input
|
||||
type="radio"
|
||||
class="radio"
|
||||
class:small
|
||||
|
@ -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} />
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user