592 lines
21 KiB
Odin
592 lines
21 KiB
Odin
package main
|
|
|
|
import "core:fmt"
|
|
import "core:os"
|
|
import "core:strings"
|
|
import "core:sync"
|
|
import "core:thread"
|
|
import "core:time"
|
|
|
|
// Git history browser for the bottom panel's Git tab. History and commit
|
|
// details come from spawned `git` processes whose output is read on a
|
|
// background thread and parsed in the per-frame pump, so the UI never blocks.
|
|
|
|
GIT_PANEL_LOG_LIMIT :: 200
|
|
GIT_PANEL_DETAIL_LIMIT :: 2000
|
|
GIT_PANEL_ROW_HEIGHT :: 16
|
|
|
|
Git_Commit :: struct {
|
|
hash: string,
|
|
date: string,
|
|
author: string,
|
|
subject: string,
|
|
}
|
|
|
|
Git_Load_Kind :: enum {
|
|
None,
|
|
Log,
|
|
Show,
|
|
Diff,
|
|
}
|
|
|
|
Git_File :: struct {
|
|
status: u8, // M/A/D/R/C/T
|
|
path: string,
|
|
}
|
|
|
|
// Reusable async runner for git commands: output is read on a background
|
|
// thread; poll it each frame until the full output is available.
|
|
Git_Loader :: struct {
|
|
process: os.Process,
|
|
process_active: bool,
|
|
stdout: ^os.File,
|
|
reader: ^thread.Thread,
|
|
reader_done: bool,
|
|
mutex: sync.Mutex,
|
|
pending: [dynamic]u8,
|
|
raw: [dynamic]u8,
|
|
active: bool,
|
|
}
|
|
|
|
git_loader_destroy :: proc(loader: ^Git_Loader) {
|
|
git_loader_cancel(loader)
|
|
delete(loader.pending)
|
|
delete(loader.raw)
|
|
}
|
|
|
|
git_loader_cancel :: proc(loader: ^Git_Loader) {
|
|
if !loader.active do return
|
|
if loader.process_active {
|
|
_ = os.process_terminate(loader.process)
|
|
_, _ = os.process_wait(loader.process)
|
|
loader.process_active = false
|
|
}
|
|
if loader.reader != nil {
|
|
// The child is dead, so the reader sees EOF and exits.
|
|
thread.join(loader.reader)
|
|
thread.destroy(loader.reader)
|
|
loader.reader = nil
|
|
}
|
|
if loader.stdout != nil {
|
|
_ = os.close(loader.stdout)
|
|
loader.stdout = nil
|
|
}
|
|
clear(&loader.pending)
|
|
clear(&loader.raw)
|
|
loader.active = false
|
|
loader.reader_done = false
|
|
}
|
|
|
|
git_loader_reader_thread :: proc(data: rawptr) {
|
|
loader := (^Git_Loader)(data)
|
|
buf: [4096]u8
|
|
for {
|
|
n, err := os.read(loader.stdout, buf[:])
|
|
if err != nil || n <= 0 do break
|
|
if sync.mutex_guard(&loader.mutex) {
|
|
for b in buf[:n] {
|
|
append(&loader.pending, b)
|
|
}
|
|
}
|
|
}
|
|
loader.reader_done = true
|
|
}
|
|
|
|
// stderr shares the pipe so git errors are visible in the output.
|
|
git_loader_spawn :: proc(loader: ^Git_Loader, command: []string) -> bool {
|
|
git_loader_cancel(loader)
|
|
|
|
stdout_r, stdout_w, pipe_err := os.pipe()
|
|
if pipe_err != nil do return false
|
|
process, start_err := os.process_start(os.Process_Desc{
|
|
command = command,
|
|
stdout = stdout_w,
|
|
stderr = stdout_w,
|
|
})
|
|
_ = os.close(stdout_w)
|
|
if start_err != nil {
|
|
_ = os.close(stdout_r)
|
|
return false
|
|
}
|
|
|
|
loader.process = process
|
|
loader.process_active = true
|
|
loader.stdout = stdout_r
|
|
loader.active = true
|
|
loader.reader_done = false
|
|
loader.reader = thread.create_and_start_with_data(rawptr(loader), git_loader_reader_thread)
|
|
return true
|
|
}
|
|
|
|
// Drains output; returns (temp-allocated output, true) once the command has
|
|
// finished, cleaning the loader up.
|
|
git_loader_poll :: proc(loader: ^Git_Loader) -> (string, bool) {
|
|
if !loader.active do return "", false
|
|
if sync.mutex_guard(&loader.mutex) {
|
|
for b in loader.pending {
|
|
append(&loader.raw, b)
|
|
}
|
|
clear(&loader.pending)
|
|
}
|
|
if !loader.reader_done do return "", false
|
|
// One more drain: the reader's final chunk may land just before done.
|
|
if sync.mutex_guard(&loader.mutex) {
|
|
for b in loader.pending {
|
|
append(&loader.raw, b)
|
|
}
|
|
clear(&loader.pending)
|
|
}
|
|
output := strings.clone(string(loader.raw[:]), context.temp_allocator)
|
|
git_loader_cancel(loader)
|
|
return output, true
|
|
}
|
|
|
|
Git_Panel :: struct {
|
|
commits: [dynamic]Git_Commit,
|
|
selected: int,
|
|
list_scroll: int,
|
|
files: [dynamic]Git_File,
|
|
file_selected: int,
|
|
file_scroll: int,
|
|
diff_text: string, // finished diff, handed to an editor tab
|
|
diff_ready: bool,
|
|
diff_path: string, // path the pending diff is for
|
|
diff_hash: string,
|
|
detail_hash: string, // hash the detail pane shows (or is loading)
|
|
message: string,
|
|
loaded_root: string, // workspace the commits were loaded from
|
|
load_kind: Git_Load_Kind,
|
|
loader: Git_Loader,
|
|
}
|
|
|
|
git_panel_destroy :: proc(panel: ^Git_Panel) {
|
|
git_loader_destroy(&panel.loader)
|
|
panel.load_kind = .None
|
|
git_panel_clear_commits(panel)
|
|
delete(panel.commits)
|
|
git_panel_clear_detail(panel)
|
|
delete(panel.files)
|
|
delete(panel.diff_text)
|
|
delete(panel.diff_path)
|
|
delete(panel.diff_hash)
|
|
delete(panel.detail_hash)
|
|
delete(panel.message)
|
|
delete(panel.loaded_root)
|
|
}
|
|
|
|
git_panel_clear_commits :: proc(panel: ^Git_Panel) {
|
|
for commit in panel.commits {
|
|
delete(commit.hash)
|
|
delete(commit.date)
|
|
delete(commit.author)
|
|
delete(commit.subject)
|
|
}
|
|
clear(&panel.commits)
|
|
}
|
|
|
|
git_panel_clear_detail :: proc(panel: ^Git_Panel) {
|
|
for file in panel.files {
|
|
delete(file.path)
|
|
}
|
|
clear(&panel.files)
|
|
panel.file_selected = 0
|
|
panel.file_scroll = 0
|
|
git_panel_clear_diff(panel)
|
|
}
|
|
|
|
git_panel_clear_diff :: proc(panel: ^Git_Panel) {
|
|
delete(panel.diff_text)
|
|
panel.diff_text = ""
|
|
panel.diff_ready = false
|
|
delete(panel.diff_path)
|
|
panel.diff_path = ""
|
|
delete(panel.diff_hash)
|
|
panel.diff_hash = ""
|
|
}
|
|
|
|
git_panel_set_message :: proc(panel: ^Git_Panel, message: string) {
|
|
delete(panel.message)
|
|
panel.message = strings.clone(message)
|
|
}
|
|
|
|
git_panel_spawn :: proc(panel: ^Git_Panel, kind: Git_Load_Kind, command: []string) {
|
|
if git_loader_spawn(&panel.loader, command) {
|
|
panel.load_kind = kind
|
|
} else {
|
|
panel.load_kind = .None
|
|
git_panel_set_message(panel, "git: failed to start (is git installed?)")
|
|
}
|
|
}
|
|
|
|
git_panel_request_log :: proc(panel: ^Git_Panel, workspace: string) {
|
|
git_panel_clear_commits(panel)
|
|
panel.selected = 0
|
|
panel.list_scroll = 0
|
|
git_panel_clear_detail(panel)
|
|
delete(panel.detail_hash)
|
|
panel.detail_hash = ""
|
|
delete(panel.loaded_root)
|
|
panel.loaded_root = strings.clone(workspace)
|
|
git_panel_set_message(panel, "Loading git history...")
|
|
|
|
limit := fmt.tprintf("-n%d", GIT_PANEL_LOG_LIMIT)
|
|
git_panel_spawn(panel, .Log, []string{"git", "-C", workspace, "log", limit, "--date=short", "--pretty=format:%h\t%ad\t%an\t%s"})
|
|
}
|
|
|
|
git_panel_request_show :: proc(panel: ^Git_Panel, workspace, hash: string) {
|
|
if panel.detail_hash == hash do return
|
|
git_panel_clear_detail(panel)
|
|
delete(panel.detail_hash)
|
|
panel.detail_hash = strings.clone(hash)
|
|
git_panel_spawn(panel, .Show, []string{"git", "-C", workspace, "show", "--name-status", "--format=", hash})
|
|
}
|
|
|
|
git_panel_request_diff :: proc(panel: ^Git_Panel, workspace, hash, path: string) {
|
|
git_panel_clear_diff(panel)
|
|
panel.diff_path = strings.clone(path)
|
|
panel.diff_hash = strings.clone(hash)
|
|
// --name-status paths are repo-root relative, but a plain pathspec is
|
|
// resolved against the workspace directory — which may be a subdirectory
|
|
// of the repository. :(top) anchors the pathspec to the repo root.
|
|
pathspec := fmt.tprintf(":(top)%s", path)
|
|
git_panel_spawn(panel, .Diff, []string{"git", "-C", workspace, "show", "--format=", "--patch", hash, "--", pathspec})
|
|
}
|
|
|
|
// Loads history once per workspace; explicit refreshes go through
|
|
// git_panel_request_log (panel reopen).
|
|
git_panel_activate :: proc(panel: ^Git_Panel, workspace: string) {
|
|
if panel.loaded_root == workspace do return
|
|
git_panel_request_log(panel, workspace)
|
|
}
|
|
|
|
// Polls the loader; parses the output once the command finished. Per frame.
|
|
git_panel_pump :: proc(panel: ^Git_Panel) {
|
|
if panel.load_kind == .None do return
|
|
output, done := git_loader_poll(&panel.loader)
|
|
if !done do return
|
|
|
|
kind := panel.load_kind
|
|
panel.load_kind = .None
|
|
|
|
switch kind {
|
|
case .Log:
|
|
git_panel_parse_log(panel, output)
|
|
case .Show:
|
|
git_panel_parse_files(panel, output)
|
|
case .Diff:
|
|
git_panel_parse_diff(panel, output)
|
|
case .None:
|
|
}
|
|
}
|
|
|
|
git_panel_parse_log :: proc(panel: ^Git_Panel, output: string) {
|
|
trimmed := strings.trim_space(output)
|
|
if strings.has_prefix(trimmed, "fatal:") || strings.has_prefix(trimmed, "error:") {
|
|
git_panel_set_message(panel, "Not a git repository")
|
|
return
|
|
}
|
|
|
|
git_panel_clear_commits(panel)
|
|
lines, _ := strings.split_lines(output, context.temp_allocator)
|
|
for line in lines {
|
|
if len(strings.trim_space(line)) == 0 do continue
|
|
fields := strings.split_n(line, "\t", 4, context.temp_allocator)
|
|
if len(fields) < 4 do continue
|
|
append(&panel.commits, Git_Commit{
|
|
hash = strings.clone(fields[0]),
|
|
date = strings.clone(fields[1]),
|
|
author = strings.clone(fields[2]),
|
|
subject = strings.clone(fields[3]),
|
|
})
|
|
}
|
|
|
|
if len(panel.commits) == 0 {
|
|
git_panel_set_message(panel, "No commits")
|
|
} else {
|
|
git_panel_set_message(panel, "")
|
|
panel.selected = clamp_int(panel.selected, 0, len(panel.commits) - 1)
|
|
}
|
|
}
|
|
|
|
git_panel_parse_files :: proc(panel: ^Git_Panel, output: string) {
|
|
git_panel_clear_detail(panel)
|
|
lines, _ := strings.split_lines(output, context.temp_allocator)
|
|
for line in lines {
|
|
trimmed := strings.trim_right(line, "\r")
|
|
if len(trimmed) == 0 do continue
|
|
fields := strings.split(trimmed, "\t", context.temp_allocator)
|
|
if len(fields) < 2 do continue
|
|
status := fields[0][0]
|
|
// Renames/copies list "old<TAB>new"; show the new path.
|
|
path := fields[len(fields) - 1]
|
|
append(&panel.files, Git_File{status = status, path = strings.clone(path)})
|
|
}
|
|
panel.file_selected = -1
|
|
}
|
|
|
|
git_panel_parse_diff :: proc(panel: ^Git_Panel, output: string) {
|
|
delete(panel.diff_text)
|
|
text := output
|
|
if len(strings.trim_space(text)) == 0 {
|
|
text = "(no changes for this file in this commit)\n"
|
|
}
|
|
panel.diff_text = strings.clone(text)
|
|
panel.diff_ready = true
|
|
}
|
|
|
|
// The editor tab name for a finished diff: "<basename> @ <hash>".
|
|
git_panel_diff_tab_name :: proc(panel: ^Git_Panel, allocator := context.temp_allocator) -> string {
|
|
base := panel.diff_path
|
|
if slash := strings.last_index_byte(base, '/'); slash >= 0 {
|
|
base = base[slash + 1:]
|
|
}
|
|
return fmt.aprintf("%s @ %s", base, panel.diff_hash, allocator = allocator)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Layout inside the bottom panel body
|
|
|
|
// Left column: commit list; right column: diff of the selected commit.
|
|
git_panel_list_width :: proc(panel_width: f32) -> f32 {
|
|
return max_f32(min_f32(panel_width * 0.4, 520), 240)
|
|
}
|
|
|
|
git_panel_visible_rows :: proc(body_height: f32) -> int {
|
|
return max_int(int((body_height - 8) / GIT_PANEL_ROW_HEIGHT), 1)
|
|
}
|
|
|
|
// Vertical layout of the detail pane: commit header, then the changed-file
|
|
// list. Shared by render, click and wheel handling.
|
|
Git_Detail_Layout :: struct {
|
|
files_y: f32,
|
|
file_rows: int,
|
|
}
|
|
|
|
git_detail_layout :: proc(panel: ^Git_Panel, body_y, body_height: f32) -> Git_Detail_Layout {
|
|
layout: Git_Detail_Layout
|
|
header_height := f32(2 * GIT_PANEL_ROW_HEIGHT + 6)
|
|
layout.files_y = body_y + header_height
|
|
layout.file_rows = max_int(int((body_y + body_height - layout.files_y - 4) / GIT_PANEL_ROW_HEIGHT), 1)
|
|
return layout
|
|
}
|
|
|
|
git_status_color :: proc(status: u8) -> (u8, u8, u8) {
|
|
switch status {
|
|
case 'A':
|
|
return 152, 195, 121
|
|
case 'D':
|
|
return 224, 108, 117
|
|
case 'R', 'C':
|
|
return 97, 175, 239
|
|
case 'M', 'T':
|
|
return 229, 192, 123
|
|
}
|
|
return 150, 154, 162
|
|
}
|
|
|
|
render_git_panel_body :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, panel: ^Git_Panel, x, y, width, height: f32) {
|
|
if len(panel.message) > 0 && len(panel.commits) == 0 {
|
|
gpu_text_limited(gpu, x + 14, y + 8, panel.message, int((width - 28) / max_f32(gpu.font_advance, 1)), 150, 154, 162, 255)
|
|
return
|
|
}
|
|
|
|
advance := max_f32(gpu.font_advance, 1)
|
|
list_width := git_panel_list_width(width)
|
|
visible := git_panel_visible_rows(height)
|
|
panel.list_scroll = clamp_int(panel.list_scroll, 0, max_int(len(panel.commits) - visible, 0))
|
|
|
|
// Commit list.
|
|
list_columns := max_int(int((list_width - 24) / advance), 8)
|
|
row_y := y + 4
|
|
for offset in 0 ..< visible {
|
|
index := panel.list_scroll + offset
|
|
if index >= len(panel.commits) do break
|
|
commit := panel.commits[index]
|
|
row_hovered := view.mouse_x >= x && view.mouse_x < x + list_width && view.mouse_y >= row_y && view.mouse_y < row_y + GIT_PANEL_ROW_HEIGHT
|
|
if index == panel.selected {
|
|
gpu_rect(gpu, x + 4, row_y, list_width - 8, GIT_PANEL_ROW_HEIGHT, 49, 56, 70, 255)
|
|
gpu_rect(gpu, x + 4, row_y, 2, GIT_PANEL_ROW_HEIGHT, 77, 155, 230, 255)
|
|
} else if row_hovered {
|
|
gpu_rect(gpu, x + 4, row_y, list_width - 8, GIT_PANEL_ROW_HEIGHT, 42, 44, 50, 255)
|
|
}
|
|
hash_width := f32(len(commit.hash)) * advance
|
|
date_width := f32(len(commit.date)) * advance
|
|
gpu_text(gpu, x + 12, row_y + 2, commit.hash, 120, 190, 250, 255)
|
|
gpu_text(gpu, x + 12 + hash_width + advance, row_y + 2, commit.date, 150, 154, 162, 255)
|
|
subject_x := x + 12 + hash_width + date_width + 2 * advance
|
|
subject_columns := max_int(int((x + list_width - 12 - subject_x) / advance), 4)
|
|
gpu_text_limited(gpu, subject_x, row_y + 2, commit.subject, subject_columns, 218, 222, 230, 255)
|
|
row_y += GIT_PANEL_ROW_HEIGHT
|
|
}
|
|
|
|
// Divider + detail pane.
|
|
detail_x := x + list_width
|
|
gpu_rect(gpu, detail_x, y, 1, height, 48, 50, 56, 255)
|
|
detail_columns := max_int(int((width - list_width - 28) / advance), 8)
|
|
|
|
if panel.selected < 0 || panel.selected >= len(panel.commits) do return
|
|
if len(panel.detail_hash) == 0 {
|
|
gpu_text_limited(gpu, detail_x + 14, y + 8, "Select a commit to see its changes", detail_columns, 150, 154, 162, 255)
|
|
return
|
|
}
|
|
|
|
// Commit header from the already-parsed commit entry.
|
|
commit := panel.commits[panel.selected]
|
|
gpu_text_limited(gpu, detail_x + 14, y + 4, commit.subject, detail_columns, 220, 223, 228, 255)
|
|
meta := fmt.tprintf("%s %s %s", commit.hash, commit.date, commit.author)
|
|
gpu_text_limited(gpu, detail_x + 14, y + 4 + GIT_PANEL_ROW_HEIGHT, meta, detail_columns, 150, 154, 162, 255)
|
|
|
|
layout := git_detail_layout(panel, y, height)
|
|
|
|
// Changed files; clicking one opens its diff in an editor tab.
|
|
if len(panel.files) == 0 {
|
|
hint := "Loading commit..." if panel.load_kind == .Show else "No changed files"
|
|
gpu_text_limited(gpu, detail_x + 14, layout.files_y, hint, detail_columns, 150, 154, 162, 255)
|
|
return
|
|
}
|
|
panel.file_scroll = clamp_int(panel.file_scroll, 0, max_int(len(panel.files) - layout.file_rows, 0))
|
|
file_y := layout.files_y
|
|
for offset in 0 ..< layout.file_rows {
|
|
index := panel.file_scroll + offset
|
|
if index >= len(panel.files) do break
|
|
file := panel.files[index]
|
|
row_hovered := view.mouse_x >= detail_x && view.mouse_x < x + width && view.mouse_y >= file_y && view.mouse_y < file_y + GIT_PANEL_ROW_HEIGHT
|
|
if index == panel.file_selected {
|
|
gpu_rect(gpu, detail_x + 4, file_y, width - list_width - 8, GIT_PANEL_ROW_HEIGHT, 49, 56, 70, 255)
|
|
gpu_rect(gpu, detail_x + 4, file_y, 2, GIT_PANEL_ROW_HEIGHT, 77, 155, 230, 255)
|
|
} else if row_hovered {
|
|
gpu_rect(gpu, detail_x + 4, file_y, width - list_width - 8, GIT_PANEL_ROW_HEIGHT, 42, 44, 50, 255)
|
|
}
|
|
status := [1]u8{file.status}
|
|
red, green, blue := git_status_color(file.status)
|
|
gpu_text(gpu, detail_x + 14, file_y + 2, string(status[:]), red, green, blue, 255)
|
|
gpu_text_limited(gpu, detail_x + 14 + 2 * advance, file_y + 2, file.path, max_int(detail_columns - 2, 4), 218, 222, 230, 255)
|
|
file_y += GIT_PANEL_ROW_HEIGHT
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Self-test: `editor --git-selftest [dir]` loads history + a commit diff from
|
|
// a real repository through the async loader and reports what it parsed.
|
|
|
|
git_panel_selftest :: proc(workspace: string) -> bool {
|
|
panel := Git_Panel{}
|
|
defer git_panel_destroy(&panel)
|
|
|
|
wait_for_load :: proc(panel: ^Git_Panel) -> bool {
|
|
for _ in 0 ..< 200 {
|
|
git_panel_pump(panel)
|
|
if panel.load_kind == .None do return true
|
|
time.sleep(50 * time.Millisecond)
|
|
}
|
|
return false
|
|
}
|
|
|
|
git_panel_request_log(&panel, workspace)
|
|
if !wait_for_load(&panel) {
|
|
fmt.println("FAIL git log timed out")
|
|
return false
|
|
}
|
|
if len(panel.commits) == 0 {
|
|
fmt.printf("FAIL no commits parsed (%s)\n", panel.message)
|
|
return false
|
|
}
|
|
first := panel.commits[0]
|
|
fmt.printf("ok %d commits; first: %s %s %s %q\n", len(panel.commits), first.hash, first.date, first.author, first.subject)
|
|
|
|
git_panel_request_show(&panel, workspace, first.hash)
|
|
if !wait_for_load(&panel) {
|
|
fmt.println("FAIL git show timed out")
|
|
return false
|
|
}
|
|
if len(panel.files) == 0 {
|
|
fmt.println("FAIL changed files not parsed")
|
|
return false
|
|
}
|
|
fmt.printf("ok %d changed files; first: %c %s\n", len(panel.files), rune(panel.files[0].status), panel.files[0].path)
|
|
|
|
// Simulate clicking every file of every commit; each diff must load
|
|
// with real contents.
|
|
checked := 0
|
|
for commit_index in 0 ..< len(panel.commits) {
|
|
hash := strings.clone(panel.commits[commit_index].hash, context.temp_allocator)
|
|
delete(panel.detail_hash)
|
|
panel.detail_hash = ""
|
|
git_panel_request_show(&panel, workspace, hash)
|
|
if !wait_for_load(&panel) {
|
|
fmt.printf("FAIL git show timed out for %s\n", hash)
|
|
return false
|
|
}
|
|
for file_index in 0 ..< len(panel.files) {
|
|
git_panel_request_diff(&panel, workspace, panel.detail_hash, panel.files[file_index].path)
|
|
if !wait_for_load(&panel) {
|
|
fmt.printf("FAIL diff timed out: %s %s\n", hash, panel.files[file_index].path)
|
|
return false
|
|
}
|
|
if !panel.diff_ready || !strings.has_prefix(panel.diff_text, "diff --git") {
|
|
fmt.printf("FAIL empty diff: %s %s -> %q\n", hash, panel.files[file_index].path, panel.diff_text[:min_int(len(panel.diff_text), 60)])
|
|
return false
|
|
}
|
|
checked += 1
|
|
}
|
|
}
|
|
fmt.printf("ok %d file diffs loaded across %d commits\n", checked, len(panel.commits))
|
|
|
|
// The split-view parser must produce aligned rows; use a modification
|
|
// diff (HEAD commit) so context, removed and added rows all appear.
|
|
git_panel_request_show(&panel, workspace, panel.commits[0].hash)
|
|
if !wait_for_load(&panel) || len(panel.files) == 0 {
|
|
fmt.println("FAIL reloading HEAD commit")
|
|
return false
|
|
}
|
|
git_panel_request_diff(&panel, workspace, panel.detail_hash, panel.files[0].path)
|
|
if !wait_for_load(&panel) {
|
|
fmt.println("FAIL reloading HEAD diff")
|
|
return false
|
|
}
|
|
doc := diff_document_make(panel.diff_text)
|
|
defer diff_document_destroy(doc)
|
|
headers, contexts, removed, added, fillers, misaligned := 0, 0, 0, 0, 0, 0
|
|
for row in doc.rows {
|
|
if row.header {
|
|
headers += 1
|
|
continue
|
|
}
|
|
if row.left_kind == ' ' && row.right_kind == ' ' {
|
|
contexts += 1
|
|
if row.left_text != row.right_text do misaligned += 1
|
|
}
|
|
if row.left_kind == '-' do removed += 1
|
|
if row.right_kind == '+' do added += 1
|
|
if row.left_kind == 0 || row.right_kind == 0 do fillers += 1
|
|
}
|
|
if len(doc.rows) == 0 || headers == 0 || misaligned > 0 {
|
|
fmt.printf("FAIL diff parse: rows=%d headers=%d misaligned=%d\n", len(doc.rows), headers, misaligned)
|
|
return false
|
|
}
|
|
fmt.printf("ok split view: %d rows (%d hunks, %d ctx, %d del, %d add, %d fill)\n", len(doc.rows), headers, contexts, removed, added, fillers)
|
|
|
|
// A workspace that is a subdirectory of the repository must work too:
|
|
// that is where root-relative --name-status paths bite.
|
|
subdir := fmt.tprintf("%s/client", workspace)
|
|
if os.is_dir(subdir) {
|
|
git_panel_request_log(&panel, subdir)
|
|
if !wait_for_load(&panel) || len(panel.commits) == 0 {
|
|
fmt.println("FAIL subdir workspace: log")
|
|
return false
|
|
}
|
|
git_panel_request_show(&panel, subdir, panel.commits[0].hash)
|
|
if !wait_for_load(&panel) || len(panel.files) == 0 {
|
|
fmt.println("FAIL subdir workspace: files")
|
|
return false
|
|
}
|
|
git_panel_request_diff(&panel, subdir, panel.detail_hash, panel.files[0].path)
|
|
if !wait_for_load(&panel) || !strings.has_prefix(panel.diff_text, "diff --git") {
|
|
fmt.printf("FAIL subdir workspace: diff -> %q\n", panel.diff_text[:min_int(len(panel.diff_text), 60)])
|
|
return false
|
|
}
|
|
fmt.printf("ok subdir workspace diff: %d bytes for %s\n", len(panel.diff_text), panel.diff_path)
|
|
}
|
|
fmt.println("git selftest: PASS")
|
|
return true
|
|
}
|