Merge branch 'master' into sc-vbranch-commits

This commit is contained in:
Scott Chacon 2023-06-21 10:50:46 +02:00 committed by GitHub
commit 645bef29b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 315 additions and 37 deletions

View File

@ -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,

View File

@ -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![],
}],
}
}

View File

@ -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![],
}],
}
}

View File

@ -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![],
}],
}
}

View File

@ -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(),

View File

@ -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![],
}],
}
}

View File

@ -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![],
}],
}
}

View File

@ -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![],
}],
}
}

View 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>

View 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>

View File

@ -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';

View File

@ -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

View File

@ -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>