mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2025-01-08 19:06:38 +03:00
Merge branch 'master' into sc-vbranch-commits
This commit is contained in:
commit
645bef29b6
@ -4,6 +4,163 @@ mod writer;
|
||||
pub use reader::BranchReader as Reader;
|
||||
pub use writer::BranchWriter as Writer;
|
||||
|
||||
use std::{cmp, fmt, ops, path};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub struct Ownership {
|
||||
pub file_path: path::PathBuf,
|
||||
pub ranges: Vec<ops::RangeInclusive<usize>>,
|
||||
}
|
||||
|
||||
impl From<&String> for Ownership {
|
||||
fn from(value: &String) -> Self {
|
||||
Self {
|
||||
file_path: value.into(),
|
||||
ranges: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Ownership {
|
||||
fn from(value: String) -> Self {
|
||||
Self {
|
||||
file_path: value.into(),
|
||||
ranges: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl cmp::PartialOrd for Ownership {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
self.file_path.partial_cmp(&other.file_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl cmp::Ord for Ownership {
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||
self.file_path.cmp(&other.file_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ownership {
|
||||
fn parse_range(s: &str) -> Result<ops::RangeInclusive<usize>> {
|
||||
let mut range = s.split('-');
|
||||
if range.clone().count() != 2 {
|
||||
return Err(anyhow!("invalid range: {}", s));
|
||||
}
|
||||
let start = range
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.context(format!("failed to parse start of range: {}", s))?;
|
||||
let end = range
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse::<usize>()
|
||||
.context(format!("failed to parse end of range: {}", s))?;
|
||||
Ok(start..=end)
|
||||
}
|
||||
|
||||
pub fn parse_string(s: &str) -> Result<Self> {
|
||||
let mut parts = s.split(':');
|
||||
let file_path = parts.next().unwrap();
|
||||
let ranges = match parts.next() {
|
||||
Some(raw_ranges) => raw_ranges
|
||||
.split(',')
|
||||
.map(Self::parse_range)
|
||||
.collect::<Result<Vec<ops::RangeInclusive<usize>>>>(),
|
||||
None => Ok(vec![]),
|
||||
}
|
||||
.context(format!("failed to parse ownership ranges: {}", s))?;
|
||||
Ok(Self {
|
||||
file_path: path::PathBuf::from(file_path),
|
||||
ranges,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ownership_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_ownership() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs:1-2,4-5").unwrap();
|
||||
assert_eq!(
|
||||
ownership,
|
||||
Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
ranges: vec![1..=2, 4..=5]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ownership_no_ranges() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs").unwrap();
|
||||
assert_eq!(
|
||||
ownership,
|
||||
Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
ranges: vec![]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ownership_invalid_range() {
|
||||
let ownership = Ownership::parse_string("foo/bar.rs:1-2,4-5-6");
|
||||
assert!(ownership.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ownership_to_from_string() {
|
||||
let ownership = Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
ranges: vec![1..=2, 4..=5],
|
||||
};
|
||||
assert_eq!(ownership.to_string(), "foo/bar.rs:1-2,4-5".to_string());
|
||||
assert_eq!(
|
||||
Ownership::parse_string(&ownership.to_string()).unwrap(),
|
||||
ownership
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ownership_to_from_string_no_ranges() {
|
||||
let ownership = Ownership {
|
||||
file_path: path::PathBuf::from("foo/bar.rs"),
|
||||
ranges: vec![],
|
||||
};
|
||||
assert_eq!(ownership.to_string(), "foo/bar.rs".to_string());
|
||||
assert_eq!(
|
||||
Ownership::parse_string(&ownership.to_string()).unwrap(),
|
||||
ownership
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ownership {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.ranges.is_empty() {
|
||||
write!(f, "{}", self.file_path.to_str().unwrap())
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}:{}",
|
||||
self.file_path.to_str().unwrap(),
|
||||
self.ranges
|
||||
.iter()
|
||||
.map(|r| format!("{}-{}", r.start(), r.end()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Branch {
|
||||
pub id: String,
|
||||
@ -14,7 +171,7 @@ pub struct Branch {
|
||||
pub updated_timestamp_ms: u128,
|
||||
pub tree: git2::Oid, // last git tree written to a session, or merge base tree if this is new. use this for delta calculation from the session data
|
||||
pub head: git2::Oid,
|
||||
pub ownership: Vec<String>,
|
||||
pub ownership: Vec<Ownership>,
|
||||
}
|
||||
|
||||
impl TryFrom<&dyn crate::reader::Reader> for Branch {
|
||||
@ -69,17 +226,23 @@ impl TryFrom<&dyn crate::reader::Reader> for Branch {
|
||||
format!("meta/updated_timestamp_ms: {}", e),
|
||||
))
|
||||
})?;
|
||||
|
||||
let ownership_string = reader.read_string("meta/ownership").map_err(|e| {
|
||||
crate::reader::Error::IOError(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("meta/ownership: {}", e),
|
||||
))
|
||||
})?;
|
||||
// convert ownership string to Vec<String>
|
||||
let ownership = ownership_string
|
||||
.split('\n')
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
.lines()
|
||||
.map(Ownership::parse_string)
|
||||
.collect::<Result<Vec<Ownership>>>()
|
||||
.map_err(|e| {
|
||||
crate::reader::Error::IOError(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("meta/ownership: {}", e),
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
|
@ -35,7 +35,9 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::{gb_repository, projects, sessions, storage, users};
|
||||
use crate::{
|
||||
gb_repository, projects, sessions, storage, users, virtual_branches::branch::Ownership,
|
||||
};
|
||||
|
||||
use super::{super::Writer, *};
|
||||
|
||||
@ -62,7 +64,10 @@ mod tests {
|
||||
unsafe { TEST_INDEX + 10 }
|
||||
))
|
||||
.unwrap(),
|
||||
ownership: vec![format!("file/{}", unsafe { TEST_INDEX })],
|
||||
ownership: vec![Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ impl<'writer> BranchWriter<'writer> {
|
||||
let ownership = branch
|
||||
.ownership
|
||||
.iter()
|
||||
.map(|user| user.to_string())
|
||||
.map(|ownership| ownership.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
@ -118,7 +118,7 @@ mod tests {
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::{projects, storage, users};
|
||||
use crate::{projects, storage, users, virtual_branches::branch};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -145,7 +145,10 @@ mod tests {
|
||||
unsafe { TEST_INDEX + 10 }
|
||||
))
|
||||
.unwrap(),
|
||||
ownership: vec![format!("file/{}", unsafe { TEST_INDEX })],
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,10 @@ mod tests {
|
||||
unsafe { TEST_INDEX + 10 }
|
||||
))
|
||||
.unwrap(),
|
||||
ownership: vec![format!("file/{}", unsafe { TEST_INDEX })],
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,11 +140,19 @@ pub fn move_files(
|
||||
for path in paths {
|
||||
let mut source_branch = virtual_branches
|
||||
.iter()
|
||||
.find(|b| b.ownership.contains(path))
|
||||
.find(|b| {
|
||||
b.ownership
|
||||
.iter()
|
||||
.map(|o| o.file_path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.contains(path)
|
||||
})
|
||||
.context(format!("failed to find source branch for {}", path))?
|
||||
.clone();
|
||||
|
||||
source_branch.ownership.retain(|f| f != path);
|
||||
source_branch
|
||||
.ownership
|
||||
.retain(|f| !f.file_path.display().to_string().eq(path));
|
||||
source_branch.ownership.sort();
|
||||
source_branch.ownership.dedup();
|
||||
|
||||
@ -152,7 +160,7 @@ pub fn move_files(
|
||||
.write(&source_branch)
|
||||
.context(format!("failed to write source branch for {}", path))?;
|
||||
|
||||
target_branch.ownership.push(path.to_string());
|
||||
target_branch.ownership.push(path.into());
|
||||
target_branch.ownership.sort();
|
||||
target_branch.ownership.dedup();
|
||||
|
||||
@ -324,8 +332,8 @@ pub fn get_status_by_branch(
|
||||
for file_path in &all_files {
|
||||
let mut file_found = false;
|
||||
for branch in &virtual_branches {
|
||||
for file in &branch.ownership {
|
||||
if file.eq(file_path) {
|
||||
for ownership in &branch.ownership {
|
||||
if ownership.file_path.display().to_string().eq(file_path) {
|
||||
file_found = true;
|
||||
}
|
||||
}
|
||||
@ -340,7 +348,12 @@ pub fn get_status_by_branch(
|
||||
if !new_ownership.is_empty() {
|
||||
// in this case, lets add any newly changed files to the first branch we see and persist it
|
||||
let mut branch = branch.clone();
|
||||
branch.ownership.extend(new_ownership.clone());
|
||||
branch
|
||||
.ownership
|
||||
.extend(new_ownership.iter().map(|file| branch::Ownership {
|
||||
file_path: file.into(),
|
||||
ranges: vec![],
|
||||
}));
|
||||
new_ownership.clear();
|
||||
|
||||
// ok, write the updated data back
|
||||
@ -348,6 +361,7 @@ pub fn get_status_by_branch(
|
||||
writer.write(&branch).context("failed to write branch")?;
|
||||
|
||||
for file in branch.ownership {
|
||||
let file = file.file_path.display().to_string();
|
||||
if all_files.contains(&file) {
|
||||
let filehunks = result.get(&file).unwrap();
|
||||
let vfile = VirtualBranchFile {
|
||||
@ -361,8 +375,9 @@ pub fn get_status_by_branch(
|
||||
}
|
||||
} else {
|
||||
for file in &branch.ownership {
|
||||
if all_files.contains(file) {
|
||||
match result.get(file) {
|
||||
let file = file.file_path.display().to_string();
|
||||
if all_files.contains(&file) {
|
||||
match result.get(&file) {
|
||||
Some(filehunks) => {
|
||||
let vfile = VirtualBranchFile {
|
||||
id: file.clone(),
|
||||
|
@ -66,7 +66,10 @@ mod tests {
|
||||
unsafe { TEST_INDEX + 10 }
|
||||
))
|
||||
.unwrap(),
|
||||
ownership: vec![format!("file/{}", unsafe { TEST_INDEX })],
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,10 @@ mod tests {
|
||||
unsafe { TEST_INDEX + 10 }
|
||||
))
|
||||
.unwrap(),
|
||||
ownership: vec![format!("file/{}", unsafe { TEST_INDEX })],
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,8 @@ use anyhow::Result;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::{
|
||||
deltas, gb_repository, project_repository, projects, sessions, storage, users, virtual_branches,
|
||||
deltas, gb_repository, project_repository, projects, sessions, storage, users,
|
||||
virtual_branches::{self, branch},
|
||||
};
|
||||
|
||||
use super::project_file_change::Handler;
|
||||
@ -44,7 +45,10 @@ fn test_branch() -> virtual_branches::branch::Branch {
|
||||
unsafe { TEST_INDEX + 10 }
|
||||
))
|
||||
.unwrap(),
|
||||
ownership: vec![format!("file/{}", unsafe { TEST_INDEX })],
|
||||
ownership: vec![branch::Ownership {
|
||||
file_path: format!("file/{}", unsafe { TEST_INDEX }).into(),
|
||||
ranges: vec![],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
33
src/lib/icons/IconTriangleDown.svelte
Normal file
33
src/lib/icons/IconTriangleDown.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<!-- <svg
|
||||
class={className}
|
||||
width="16"
|
||||
height="12"
|
||||
viewBox="0 0 16 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.707 5.293L10.707 0.293C10.316 -0.0979999 9.684 -0.0979999 9.293 0.293C8.902 0.684 8.902 1.316 9.293 1.707L12.586 5H1C0.447 5 0 5.448 0 6C0 6.552 0.447 7 1 7H12.586L9.293 10.293C8.902 10.684 8.902 11.316 9.293 11.707C9.488 11.902 9.744 12 10 12C10.256 12 10.512 11.902 10.707 11.707L15.707 6.707C16.098 6.316 16.098 5.684 15.707 5.293Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg> -->
|
||||
<svg
|
||||
class={className}
|
||||
width="9"
|
||||
height="5"
|
||||
viewBox="0 0 9 5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.51628 3.98009e-07L1.31965 -1.43717e-07C0.568658 -2.09371e-07 0.147978 0.75351 0.611959 1.2676L3.71027 4.70055C4.07062 5.09982 4.76532 5.09982 5.12566 4.70055L8.22398 1.2676C8.68796 0.753511 8.26728 4.63664e-07 7.51628 3.98009e-07Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
34
src/lib/icons/IconTriangleUp.svelte
Normal file
34
src/lib/icons/IconTriangleUp.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<!-- <svg
|
||||
class={className}
|
||||
width="16"
|
||||
height="12"
|
||||
viewBox="0 0 16 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.707 5.293L10.707 0.293C10.316 -0.0979999 9.684 -0.0979999 9.293 0.293C8.902 0.684 8.902 1.316 9.293 1.707L12.586 5H1C0.447 5 0 5.448 0 6C0 6.552 0.447 7 1 7H12.586L9.293 10.293C8.902 10.684 8.902 11.316 9.293 11.707C9.488 11.902 9.744 12 10 12C10.256 12 10.512 11.902 10.707 11.707L15.707 6.707C16.098 6.316 16.098 5.684 15.707 5.293Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg> -->
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
width="9"
|
||||
height="5"
|
||||
viewBox="0 0 9 5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.31965 5H7.51628C8.26728 5 8.68796 4.24649 8.22398 3.7324L5.12566 0.299447C4.76532 -0.0998156 4.07062 -0.0998156 3.71027 0.299447L0.611959 3.7324C0.147977 4.24649 0.568658 5 1.31965 5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
@ -29,3 +29,5 @@ export { default as IconEmail } from './IconEmail.svelte';
|
||||
export { default as IconBookmarkFilled } from './IconBookmarkFilled.svelte';
|
||||
export { default as IconAISparkles } from './IconAISparkles.svelte';
|
||||
export { default as IconBranch } from './IconBranch.svelte';
|
||||
export { default as IconTriangleUp } from './IconTriangleUp.svelte';
|
||||
export { default as IconTriangleDown } from './IconTriangleDown.svelte';
|
||||
|
@ -7,6 +7,7 @@
|
||||
import FileCard from './FileCard.svelte';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { IconBranch } from '$lib/icons';
|
||||
import { IconTriangleUp, IconTriangleDown } from '$lib/icons';
|
||||
|
||||
export let branchId: string;
|
||||
export let name: string;
|
||||
@ -14,6 +15,7 @@
|
||||
export let files: File[];
|
||||
export let projectId: string;
|
||||
|
||||
let allExpanded = true;
|
||||
let descriptionHeight = 0;
|
||||
let textArea: HTMLTextAreaElement;
|
||||
const dispatch = createEventDispatcher();
|
||||
@ -87,6 +89,18 @@
|
||||
value={description ? description.trim() : ''}
|
||||
on:change={updateTextArea}
|
||||
/>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
class="flex h-6 w-6 items-center justify-center"
|
||||
on:click={() => (allExpanded = !allExpanded)}
|
||||
>
|
||||
{#if allExpanded}
|
||||
<IconTriangleUp />
|
||||
{:else}
|
||||
<IconTriangleDown />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-shrink flex-col gap-y-2 overflow-y-auto rounded-lg"
|
||||
use:dndzone={{
|
||||
@ -99,7 +113,12 @@
|
||||
on:finalize={handleDndEvent}
|
||||
>
|
||||
{#each files.filter((x) => x.hunks) as file (file.id)}
|
||||
<FileCard filepath={file.path} bind:hunks={file.hunks} on:empty={handleEmpty} />
|
||||
<FileCard
|
||||
filepath={file.path}
|
||||
expanded={allExpanded}
|
||||
bind:hunks={file.hunks}
|
||||
on:empty={handleEmpty}
|
||||
/>
|
||||
{/each}
|
||||
<div
|
||||
data-dnd-ignore
|
||||
|
@ -6,13 +6,14 @@
|
||||
import type { Hunk } from './types';
|
||||
import HunkDiffViewer from './HunkDiffViewer.svelte';
|
||||
import { summarizeHunk } from '$lib/summaries';
|
||||
import { IconTriangleUp, IconTriangleDown } from '$lib/icons';
|
||||
|
||||
export let filepath: string;
|
||||
export let hunks: Hunk[];
|
||||
let zoneEl: HTMLElement;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let expanded = true;
|
||||
export let expanded = true;
|
||||
|
||||
function handleDndEvent(e: CustomEvent<DndEvent<Hunk>>) {
|
||||
hunks = e.detail.items;
|
||||
@ -51,19 +52,9 @@
|
||||
class="cursor-pointer p-2"
|
||||
>
|
||||
{#if expanded}
|
||||
<svg width="9" height="5" viewBox="0 0 9 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.31965 5H7.51628C8.26728 5 8.68796 4.24649 8.22398 3.7324L5.12566 0.299447C4.76532 -0.0998156 4.07062 -0.0998156 3.71027 0.299447L0.611959 3.7324C0.147977 4.24649 0.568658 5 1.31965 5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<IconTriangleUp />
|
||||
{:else}
|
||||
<svg width="9" height="5" viewBox="0 0 9 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.51628 3.98009e-07L1.31965 -1.43717e-07C0.568658 -2.09371e-07 0.147978 0.75351 0.611959 1.2676L3.71027 4.70055C4.07062 5.09982 4.76532 5.09982 5.12566 4.70055L8.22398 1.2676C8.68796 0.753511 8.26728 4.63664e-07 7.51628 3.98009e-07Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<IconTriangleDown />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user