gites: colorful git tree viewer

Tauri 2 + Vue app that draws the commit graph of the repository given on
the command line. Branches can be hidden one by one or by patterns,
runs of plain commits fold into a dotted line, and everything is
reachable from the keyboard.
This commit is contained in:
2026-10-14 21:11:55 +02:00
commit c05edf7b74
58 changed files with 7716 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { isRefVisible } from "../lib/filter";
import { refsUnder, type TreeNode } from "../lib/tree";
import type { FilterState, Ref, RefKind } from "../lib/types";
import EyeButton from "./EyeButton.vue";
import RefIcon from "./RefIcon.vue";
const props = defineProps<{
nodes: TreeNode[];
kind: RefKind;
depth: number;
filters: FilterState;
headRef: string | null;
/** Paths of folders the user closed. Shared by the whole tree. */
closed: Set<string>;
}>();
const emit = defineEmits<{ jump: [ref: Ref]; toggle: [names: string[]] }>();
const allHidden = (n: TreeNode) => refsUnder(n).every((r) => props.filters.hidden.includes(r.name));
const anyVisible = (n: TreeNode) => refsUnder(n).some((r) => isRefVisible(r.name, props.filters));
function toggleFolder(path: string) {
if (!props.closed.delete(path)) props.closed.add(path);
}
</script>
<template>
<template v-for="n in nodes" :key="n.path">
<div
class="item"
:class="{ off: !anyVisible(n), head: n.ref?.name === headRef, folder: n.children.length }"
:style="{ paddingLeft: 10 + depth * 14 + 'px' }"
@click="n.ref ? emit('jump', n.ref) : toggleFolder(n.path)"
>
<span
v-if="n.children.length"
class="arrow"
:class="{ open: !closed.has(n.path) }"
@click.stop="toggleFolder(n.path)"
>▸</span>
<span v-else class="arrow" />
<RefIcon v-if="n.ref" :kind="kind" />
<svg v-else class="folder-icon" viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M1.5 4a1 1 0 0 1 1-1h3.5l1.5 1.5h6a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1z" />
</svg>
<span class="name" :title="n.ref?.name ?? n.path">{{ n.label }}</span>
<EyeButton :shut="allHidden(n)" @click.stop="emit('toggle', refsUnder(n).map((r) => r.name))" />
</div>
<RefTree
v-if="n.children.length && !closed.has(n.path)"
:nodes="n.children"
:kind="kind"
:depth="depth + 1"
:filters="filters"
:head-ref="headRef"
:closed="closed"
@jump="emit('jump', $event)"
@toggle="emit('toggle', $event)"
/>
</template>
</template>
<style scoped>
.item {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 8px 2px 10px;
cursor: pointer;
color: var(--text);
}
.item:hover { background: var(--bg-hover); }
.item.off { color: var(--text-dim); opacity: 0.55; }
.item.head .name { font-weight: 600; color: #fff; }
.item.folder { color: #c5cae0; }
.folder-icon { flex: none; color: #ffd43b; opacity: 0.8; }
.arrow { width: 10px; flex: none; color: var(--text-dim); font-size: 10px; transition: transform 0.12s; }
.arrow.open { transform: rotate(90deg); }
.name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.item:hover :deep(.eye) { opacity: 1; }
</style>