mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-28 04:47:42 +03:00
Improve git committing with new screen
New commit screen with diffs pulled from Rust and summarizable by gpt4.
This commit is contained in:
parent
ffe56ab14c
commit
16601f31d3
@ -376,9 +376,14 @@ async fn git_status(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn git_wd_diff(handle: tauri::AppHandle, project_id: &str) -> Result<String, Error> {
|
||||
async fn git_wd_diff(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
) -> Result<HashMap<String, String>, Error> {
|
||||
let repo = repo_for_project(handle, project_id)?;
|
||||
let diff = repo.wd_diff().with_context(|| "Failed to get git diff")?;
|
||||
let diff = repo
|
||||
.wd_diff(100) // max 100 lines per file
|
||||
.with_context(|| "Failed to get git diff")?;
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{deltas, fs, projects, sessions, users};
|
||||
use anyhow::{Context, Result};
|
||||
use git2::{BranchType, Cred, Signature};
|
||||
use std::{collections::HashMap, env, path::Path, str::from_utf8};
|
||||
use git2::{BranchType, Cred, DiffOptions, Signature};
|
||||
use std::{collections::HashMap, env, path::Path};
|
||||
use tauri::regex::Regex;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
@ -142,24 +142,56 @@ impl Repository {
|
||||
Ok(branch.to_string())
|
||||
}
|
||||
|
||||
pub fn wd_diff(&self) -> Result<String> {
|
||||
println!("diffing");
|
||||
pub fn wd_diff(&self, max_lines: usize) -> Result<HashMap<String, String>> {
|
||||
let repo = &self.git_repository;
|
||||
let head = repo.head()?;
|
||||
let tree = head.peel_to_tree()?;
|
||||
let diff = repo.diff_tree_to_workdir_with_index(Some(&tree), None)?;
|
||||
let mut buf = String::new();
|
||||
|
||||
// Prepare our diff options based on the arguments given
|
||||
let mut opts = DiffOptions::new();
|
||||
opts.recurse_untracked_dirs(true)
|
||||
.include_untracked(true)
|
||||
.include_ignored(true);
|
||||
|
||||
let diff = repo.diff_tree_to_workdir(Some(&tree), Some(&mut opts))?;
|
||||
|
||||
let mut result = HashMap::new();
|
||||
let mut results = String::new();
|
||||
|
||||
let mut current_line_count = 0;
|
||||
let mut last_path = String::new();
|
||||
|
||||
diff.print(git2::DiffFormat::Patch, |delta, hunk, line| {
|
||||
buf.push_str(&format!(
|
||||
"{:?} {}",
|
||||
delta.status(),
|
||||
delta.new_file().path().unwrap().to_str().unwrap()
|
||||
));
|
||||
buf.push_str(from_utf8(line.content()).unwrap());
|
||||
buf.push_str("\n");
|
||||
let new_path = delta
|
||||
.new_file()
|
||||
.path()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
print!(
|
||||
"{} {}",
|
||||
new_path,
|
||||
std::str::from_utf8(line.content()).unwrap()
|
||||
);
|
||||
if new_path != last_path {
|
||||
result.insert(last_path.clone(), results.clone());
|
||||
results = String::new();
|
||||
current_line_count = 0;
|
||||
last_path = new_path.clone();
|
||||
}
|
||||
if current_line_count <= max_lines {
|
||||
match line.origin() {
|
||||
'+' | '-' | ' ' => results.push_str(&format!("{}", line.origin())),
|
||||
_ => {}
|
||||
}
|
||||
results.push_str(&format!("{}", std::str::from_utf8(line.content()).unwrap()));
|
||||
current_line_count += 1;
|
||||
}
|
||||
true
|
||||
})?;
|
||||
Ok(buf)
|
||||
result.insert(last_path.clone(), results.clone());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn switch_branch(&self, branch_name: &str) -> Result<bool> {
|
||||
@ -203,7 +235,12 @@ impl Repository {
|
||||
s if s.contains(git2::Status::WT_DELETED) => "deleted",
|
||||
s if s.contains(git2::Status::WT_RENAMED) => "renamed",
|
||||
s if s.contains(git2::Status::WT_TYPECHANGE) => "typechange",
|
||||
_ => continue,
|
||||
s if s.contains(git2::Status::INDEX_NEW) => "added",
|
||||
s if s.contains(git2::Status::INDEX_MODIFIED) => "modified",
|
||||
s if s.contains(git2::Status::INDEX_DELETED) => "deleted",
|
||||
s if s.contains(git2::Status::INDEX_RENAMED) => "renamed",
|
||||
s if s.contains(git2::Status::INDEX_TYPECHANGE) => "typechange",
|
||||
_ => "other",
|
||||
};
|
||||
files.insert(path.to_string(), istatus.to_string());
|
||||
}
|
||||
|
@ -1,49 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import { format, startOfDay } from 'date-fns';
|
||||
import type { Activity } from '$lib/sessions';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import { shortPath } from '$lib/paths';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { toHumanBranchName } from '$lib/branch';
|
||||
import { list as listDeltas } from '$lib/deltas';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { navigating } from '$app/stores';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { goto } from '$app/navigation';
|
||||
import Api from '$lib/api';
|
||||
|
||||
const api = Api({ fetch });
|
||||
const getBranch = (params: { projectId: string }) => invoke<string>('git_branch', params);
|
||||
const getDiff = (params: { projectId: string }) => invoke<string>('git_wd_diff', params);
|
||||
|
||||
export let data: LayoutData;
|
||||
$: project = data.project;
|
||||
$: filesStatus = data.filesStatus;
|
||||
$: recentActivity = data.recentActivity;
|
||||
$: orderedSessionsFromLastFourDays = data.orderedSessionsFromLastFourDays;
|
||||
$: user = data.user;
|
||||
|
||||
const commit = (params: {
|
||||
projectId: string;
|
||||
message: string;
|
||||
files: Array<string>;
|
||||
push: boolean;
|
||||
}) => invoke<boolean>('git_commit', params);
|
||||
|
||||
let latestDeltasByDateByFile: Record<number, Record<string, Delta[][]>[]> = {};
|
||||
let commitMessage: string;
|
||||
let placeholderMessage = 'Description of changes';
|
||||
let initiatedCommit = false;
|
||||
let filesSelectedForCommit: string[] = [];
|
||||
|
||||
$: if ($navigating) {
|
||||
commitMessage = '';
|
||||
filesSelectedForCommit = [];
|
||||
initiatedCommit = false;
|
||||
}
|
||||
|
||||
function gotoPlayer(filename: string) {
|
||||
if ($project) {
|
||||
@ -60,24 +33,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function doCommit() {
|
||||
if ($project) {
|
||||
commit({
|
||||
projectId: $project.id,
|
||||
message: commitMessage,
|
||||
files: filesSelectedForCommit,
|
||||
push: false
|
||||
}).then((result) => {
|
||||
toast.success('Commit successful!', {
|
||||
icon: '🎉'
|
||||
});
|
||||
commitMessage = '';
|
||||
filesSelectedForCommit = [];
|
||||
initiatedCommit = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($project) {
|
||||
latestDeltasByDateByFile = {};
|
||||
const dateSessions: Record<number, Session[]> = {};
|
||||
@ -188,27 +143,6 @@
|
||||
return sessionsByFile;
|
||||
}
|
||||
|
||||
function fetchCommitMessage() {
|
||||
if ($project && $user) {
|
||||
placeholderMessage = 'Summarizing changes...';
|
||||
console.log('FETCHING DIFF');
|
||||
getDiff({
|
||||
projectId: $project.id
|
||||
}).then((result) => {
|
||||
console.log('DIFF', result);
|
||||
api.summarize
|
||||
.commit($user?.access_token, {
|
||||
diff: result,
|
||||
uid: $project.id
|
||||
})
|
||||
.then((result) => {
|
||||
console.log(result);
|
||||
commitMessage = result.message;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// order the sessions and summarize the changes by file
|
||||
function orderedSessions(dateSessions: Record<number, Record<string, Delta[][]>[]>) {
|
||||
return Object.entries(dateSessions)
|
||||
@ -323,7 +257,13 @@
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="branch-count-container text-md hidden hover:text-blue-500 ">6 branches</div>
|
||||
<div>
|
||||
<a
|
||||
href="/projects/{$project?.id}/commit"
|
||||
class="button rounded bg-blue-600 py-2 px-3 text-white hover:bg-blue-700"
|
||||
>Commit changes</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $filesStatus.length == 0}
|
||||
@ -345,78 +285,13 @@
|
||||
<div class="rounded border border-yellow-400 bg-yellow-500 p-4 font-mono text-yellow-900">
|
||||
<ul class="w-80 truncate pl-4">
|
||||
{#each $filesStatus as activity}
|
||||
<li class={initiatedCommit ? '-ml-5' : 'list-disc'}>
|
||||
{#if initiatedCommit}
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:group={filesSelectedForCommit}
|
||||
value={activity.path}
|
||||
/>
|
||||
{/if}
|
||||
<li class="list-disc">
|
||||
{activity.status.slice(0, 1)}
|
||||
{shortPath(activity.path)}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- TODO: Button needs to be hooked up -->
|
||||
<div class="mt-2 flex flex-col">
|
||||
{#if initiatedCommit}
|
||||
<div transition:slide={{ duration: 150 }}>
|
||||
<h3 class="text-base font-semibold text-zinc-200">Commit Message</h3>
|
||||
<textarea
|
||||
rows="4"
|
||||
name="message"
|
||||
placeholder={placeholderMessage}
|
||||
bind:value={commitMessage}
|
||||
class="mb-2 block w-full rounded-md border-0 p-4 text-zinc-200 ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="w-100 flex flex-row-reverse items-center justify-between gap-4">
|
||||
{#if initiatedCommit}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="button w-[60px] rounded border border-zinc-600 py-2 text-white hover:bg-zinc-800"
|
||||
on:click={() => {
|
||||
initiatedCommit = false;
|
||||
}}>✘</button
|
||||
>
|
||||
<button
|
||||
disabled={!commitMessage || filesSelectedForCommit.length == 0}
|
||||
class="{!commitMessage || filesSelectedForCommit.length == 0
|
||||
? 'bg-zinc-800 text-zinc-600'
|
||||
: ''} button rounded bg-blue-600 py-2 px-3 text-white"
|
||||
on:click={() => {
|
||||
doCommit();
|
||||
initiatedCommit = false;
|
||||
commitMessage = '';
|
||||
}}>Commit changes</button
|
||||
>
|
||||
</div>
|
||||
<div class="w-100 align-left">
|
||||
{#if filesSelectedForCommit.length == 0}
|
||||
<div>Select at least one file.</div>
|
||||
{:else if !commitMessage}
|
||||
<div>Provide a commit message.</div>
|
||||
{:else}
|
||||
<div>Are you certain of this?</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="button rounded bg-blue-600 py-2 px-3 text-white hover:bg-blue-700"
|
||||
on:click={() => {
|
||||
filesSelectedForCommit = $filesStatus.map((file) => {
|
||||
return file.path;
|
||||
});
|
||||
fetchCommitMessage();
|
||||
initiatedCommit = true;
|
||||
}}>Commit changes</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
|
250
src/routes/projects/[projectId]/commit/+page.svelte
Normal file
250
src/routes/projects/[projectId]/commit/+page.svelte
Normal file
@ -0,0 +1,250 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { PageData } from './$types';
|
||||
import Api from '$lib/api';
|
||||
import { shortPath } from '$lib/paths';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { toHumanBranchName } from '$lib/branch';
|
||||
|
||||
const api = Api({ fetch });
|
||||
|
||||
export let data: PageData;
|
||||
const { project, user, filesStatus } = data;
|
||||
|
||||
let commitSubject: string;
|
||||
let placeholderSubject = 'One line summary of changes';
|
||||
let commitMessage: string;
|
||||
let placeholderMessage = 'Optional fuller description of changes';
|
||||
let messageRows = 6;
|
||||
let filesSelectedForCommit: string[] = [];
|
||||
|
||||
const commit = (params: {
|
||||
projectId: string;
|
||||
message: string;
|
||||
files: Array<string>;
|
||||
push: boolean;
|
||||
}) => invoke<boolean>('git_commit', params);
|
||||
|
||||
function doCommit() {
|
||||
if ($project) {
|
||||
if (commitMessage) {
|
||||
commitSubject = commitSubject + '\n\n' + commitMessage;
|
||||
}
|
||||
commit({
|
||||
projectId: $project.id,
|
||||
message: commitSubject,
|
||||
files: filesSelectedForCommit,
|
||||
push: false
|
||||
}).then((result) => {
|
||||
toast.success('Commit successful!', {
|
||||
icon: '🎉'
|
||||
});
|
||||
commitMessage = '';
|
||||
commitSubject = '';
|
||||
filesSelectedForCommit = [];
|
||||
isLoaded = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const toggleAllOff = () => {
|
||||
filesSelectedForCommit = [];
|
||||
};
|
||||
const toggleAllOn = () => {
|
||||
filesSelectedForCommit = $filesStatus.map((file) => {
|
||||
return file.path;
|
||||
});
|
||||
};
|
||||
const showMessage = (message: string) => {
|
||||
generatedMessage = undefined;
|
||||
};
|
||||
|
||||
const getDiff = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_wd_diff', params);
|
||||
const getBranch = (params: { projectId: string }) => invoke<string>('git_branch', params);
|
||||
|
||||
let gitBranch = <string | undefined>undefined;
|
||||
let gitDiff = <string | undefined>undefined;
|
||||
let generatedMessage = <string | undefined>undefined;
|
||||
let isLoaded = false;
|
||||
|
||||
$: if ($project) {
|
||||
if (!isLoaded) {
|
||||
getBranch({ projectId: $project?.id }).then((branch) => {
|
||||
gitBranch = branch;
|
||||
filesSelectedForCommit = $filesStatus.map((file) => {
|
||||
return file.path;
|
||||
});
|
||||
});
|
||||
getDiff({ projectId: $project?.id }).then((diff) => {
|
||||
gitDiff = diff;
|
||||
});
|
||||
isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
let loadingPercent = 0;
|
||||
function fetchCommitMessage() {
|
||||
if ($project && $user) {
|
||||
// make diff from keys of gitDiff matching entries in filesSelectedForCommit
|
||||
const partialDiff = Object.fromEntries(
|
||||
Object.entries(gitDiff).filter(([key]) => filesSelectedForCommit.includes(key))
|
||||
);
|
||||
console.log(partialDiff);
|
||||
// convert to string
|
||||
const diff = Object.values(partialDiff).join('\n').slice(0, 5000); // limit for summary
|
||||
console.log(diff);
|
||||
|
||||
placeholderMessage = 'Summarizing changes...';
|
||||
generatedMessage = 'loading';
|
||||
loadingPercent = 0;
|
||||
// every second update loadingPercent by 8%
|
||||
const interval = setInterval(() => {
|
||||
loadingPercent += 6.25;
|
||||
if (loadingPercent >= 100) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
api.summarize
|
||||
.commit($user?.access_token, {
|
||||
diff: diff,
|
||||
uid: $project.id
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.message) {
|
||||
// split result into subject and message (first line is subject)
|
||||
commitSubject = result.message.split('\n')[0];
|
||||
commitMessage = result.message.split('\n').slice(2).join('\n');
|
||||
generatedMessage = result.message;
|
||||
// set messageRows as a function of the number of chars in the message
|
||||
messageRows = Math.ceil(commitMessage.length / 75) + 3;
|
||||
}
|
||||
loadingPercent = 100;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-col w-[500px] min-w-[500px] flex-shrink-0 p-2">
|
||||
<div
|
||||
class="button group mb-2 flex max-w-[500px] rounded border border-zinc-600 bg-zinc-700 py-2 px-4 text-zinc-300 shadow"
|
||||
>
|
||||
<div class="h-4 w-4">
|
||||
<svg
|
||||
text="gray"
|
||||
aria-hidden="true"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
width="16"
|
||||
data-view-component="true"
|
||||
class="h-4 w-4 fill-zinc-400"
|
||||
>
|
||||
<path
|
||||
d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="truncate pl-2 font-mono text-zinc-300">
|
||||
{toHumanBranchName(gitBranch)}
|
||||
</div>
|
||||
<div class="carrot flex hidden items-center pl-3">
|
||||
<svg width="7" height="5" viewBox="0 0 7 5" fill="none" class="fill-zinc-400">
|
||||
<path
|
||||
d="M3.87796 4.56356C3.67858 4.79379 3.32142 4.79379 3.12204 4.56356L0.319371 1.32733C0.0389327 1.00351 0.268959 0.5 0.697336 0.5L6.30267 0.500001C6.73104 0.500001 6.96107 1.00351 6.68063 1.32733L3.87796 4.56356Z"
|
||||
fill="#A1A1AA"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div transition:slide={{ duration: 150 }}>
|
||||
<h3 class="text-base font-semibold text-zinc-200 mb-2">Commit Message</h3>
|
||||
<input
|
||||
type="text"
|
||||
name="subject"
|
||||
bind:value={commitSubject}
|
||||
placeholder={placeholderSubject}
|
||||
class="mb-2 block w-full rounded-md border-0 p-4 text-zinc-200 ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
<textarea
|
||||
rows={messageRows}
|
||||
name="message"
|
||||
placeholder={placeholderMessage}
|
||||
bind:value={commitMessage}
|
||||
class="mb-2 block w-full rounded-md border-0 p-4 text-zinc-200 ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between">
|
||||
{#if filesSelectedForCommit.length == 0}
|
||||
<div>Select at least one file.</div>
|
||||
{:else if !commitSubject}
|
||||
<div>Provide a commit message.</div>
|
||||
{:else}
|
||||
<button
|
||||
disabled={!commitSubject || filesSelectedForCommit.length == 0}
|
||||
class="{!commitSubject || filesSelectedForCommit.length == 0
|
||||
? 'bg-zinc-800 text-zinc-600'
|
||||
: ''} button rounded bg-blue-600 py-2 px-3 text-white"
|
||||
on:click={() => {
|
||||
doCommit();
|
||||
}}>Commit changes</button
|
||||
>
|
||||
{/if}
|
||||
{#if !generatedMessage}
|
||||
<a class="cursor-pointer bg-green-800 rounded p-2" on:click={fetchCommitMessage}
|
||||
>Generate a message for me.</a
|
||||
>
|
||||
{:else if generatedMessage == 'loading'}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-zinc-400">Let me take a look at these changes...</div>
|
||||
<!-- status bar filled by loadingPercent -->
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div
|
||||
class="bg-green-600 h-2.5 rounded-full"
|
||||
style="width: {Math.round(loadingPercent)}%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="rounded border border-zinc-400 bg-zinc-500 font-mono text-zinc-900">
|
||||
<div
|
||||
class="flex flex-row space-x-2 text-zinc-200 bg-zinc-800 rounded-t border-b border-zinc-600 p-2 mb-2"
|
||||
>
|
||||
<h3 class="text-base font-semibold ">File Changes</h3>
|
||||
<a href="#" class="text-yellow-200" on:click={toggleAllOn}>all</a>
|
||||
<a href="#" class="text-yellow-200" on:click={toggleAllOff}>none</a>
|
||||
</div>
|
||||
<ul class="w-80 truncate px-2 pb-2">
|
||||
{#each $filesStatus as activity}
|
||||
<li class="list-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
on:click={showMessage}
|
||||
bind:group={filesSelectedForCommit}
|
||||
value={activity.path}
|
||||
/>
|
||||
{activity.status.slice(0, 1)}
|
||||
{shortPath(activity.path)}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow p-2 h-full max-h-screen overflow-auto">
|
||||
{#if gitDiff}
|
||||
{#each Object.entries(gitDiff) as [key, value]}
|
||||
{#if key}
|
||||
<div class="p-2">{key}</div>
|
||||
<pre class="p-2 bg-zinc-900">{value}</pre>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<!-- commit message -->
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user