AmendHub

jcs

/

amend

/

amendments

/

1

Initial import of Amend through Amend!


jcs made amendment 1 11 months ago
--- amend.h Mon Oct 18 13:09:14 2021 +++ amend.h Mon Oct 18 13:09:14 2021 @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#define PROGRAM_NAME "Amend" + +#define MBAR_ID 128 + +#define APPLE_MENU_ID 128 +#define APPLE_MENU_ABOUT_ID 1 + +#define FILE_MENU_ID 129 +#define FILE_MENU_NEW_ID 1 +#define FILE_MENU_OPEN_ID 2 +#define FILE_MENU_QUIT_ID 4 + +#define EDIT_MENU_ID 130 +#define EDIT_MENU_COPY_ID 1 +#define EDIT_MENU_SELECT_ALL_ID 2 + +#define REPO_MENU_ID 131 +#define REPO_MENU_ADD_FILE_ID 1 + +#define COMMIT_LDEF_ID 128 + +#define TABWIDTH_ID 130 + +extern MenuHandle file_menu, edit_menu, repo_menu; --- browser.c Mon Oct 18 13:09:06 2021 +++ browser.c Mon Oct 18 13:09:06 2021 @@ -0,0 +1,533 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdarg.h> +#include <stdio.h> +#include <string.h> + +#include "amend.h" +#include "browser.h" +#include "committer.h" +#include "diff.h" +#include "repo.h" +#include "util.h" + +void browser_add_files(struct browser *browser); +void browser_filter_commits(struct browser *browser); + +Pattern fill_pattern; + +void +browser_idle(struct browser *browser) +{ + switch (browser->state) { + case BROWSER_STATE_IDLE: + break; + case BROWSER_STATE_ADD_FILE: + if (repo_add_file(browser->repo) == NULL) + browser->state = BROWSER_STATE_IDLE; + else + browser->state = BROWSER_STATE_UPDATE_FILE_LIST; + break; + case BROWSER_STATE_UPDATE_FILE_LIST: + browser_add_files(browser); + browser->state = BROWSER_STATE_IDLE; + break; + case BROWSER_STATE_UPDATE_COMMIT_LIST: + browser_filter_commits(browser); + browser->state = BROWSER_STATE_IDLE; + break; + case BROWSER_STATE_OPEN_COMMITTER: + committer_init(browser); + browser->state = BROWSER_STATE_COMMITTER_DO_DIFF; + break; + case BROWSER_STATE_COMMITTER_DO_DIFF: + committer_generate_diff(browser->committer); + browser->state = BROWSER_STATE_WAITING_FOR_COMMITTER; + break; + case BROWSER_STATE_WAITING_FOR_COMMITTER: + if (browser->committer) + committer_idle(browser->committer); + break; + } +} + +struct browser * +browser_init(struct repo *repo) +{ + char title[256] = { 0 }; + struct browser *browser; + Rect bounds = { 0 }, te_bounds = { 0 }; + Rect data_bounds = { 0, 0, 0, 1 }; /* tlbr */ + Point cell_size = { 0, 0 }; + Cell cell = { 0 }; + short padding = 10; + + browser = xmalloczero(sizeof(struct browser)); + browser->state = BROWSER_STATE_IDLE; + browser->repo = repo; + + GetIndPattern(&fill_pattern, sysPatListID, 22); + + /* main window */ + bounds.left = (padding / 2); + bounds.top = screenBits.bounds.top + (GetMBarHeight() * 2) - 1 + + (padding / 2); + bounds.right = screenBits.bounds.right - 1 - (padding / 2); + bounds.bottom = screenBits.bounds.bottom - 1 - (padding / 2); + + if (sprintf(title, "%s: %s", PROGRAM_NAME, + browser->repo->filename) > sizeof(title)) + err(1, "sprintf overflow!"); + CtoPstr(title); + + browser->win = NewWindow(0L, &bounds, title, false, noGrowDocProc, + (WindowPtr)-1L, true, 0); + if (!browser->win) + err(1, "Can't create window"); + SetPort(browser->win); + + /* file list */ + bounds.top = bounds.left = padding; + bounds.bottom = browser->win->portRect.bottom - + (browser->win->portRect.bottom / 2) - 2 - 20 - padding - padding; + bounds.right = 100; + + TextFont(applFont); + TextSize(10); + browser->file_list = LNew(&bounds, &data_bounds, cell_size, 0, + browser->win, true, true, false, true); + if (!browser->file_list) + err(1, "Can't create file list"); + (*(browser->file_list))->selFlags = lNoNilHilite; + + /* diff button */ + bounds.top = bounds.bottom + padding; + bounds.bottom = bounds.top + 20; + bounds.right += SCROLLBAR_WIDTH; + TextFont(applFont); + TextSize(11); + browser->diff_button = NewControl(browser->win, &bounds, + "\pGenerate Diff", true, 1, 1, 1, pushButProc | useWFont, 0L); + + /* commit list */ + bounds.top = bounds.left = padding; + bounds.bottom = browser->win->portRect.bottom - + (browser->win->portRect.bottom / 2) - 2 - padding; + bounds.left = bounds.right + padding; + bounds.right = browser->win->portRect.right - SCROLLBAR_WIDTH - padding; + + cell_size.v = (FontHeight(applFont, 9) * 2) + 2; + browser->commit_list = LNew(&bounds, &data_bounds, cell_size, + COMMIT_LDEF_ID, browser->win, true, true, false, true); + if (!browser->commit_list) + err(1, "Can't create commit list"); + (*(browser->commit_list))->selFlags = lOnlyOne; + + /* diff text */ + bounds.top = (*browser->commit_list)->rView.bottom + padding; + bounds.left = padding; + bounds.right = browser->win->portRect.right - SCROLLBAR_WIDTH - padding; + bounds.bottom = browser->win->portRect.bottom - padding; + TextFont(monaco); + TextSize(9); + te_bounds = bounds; + InsetRect(&te_bounds, 2, 2); + browser->diff_te = TENew(&te_bounds, &bounds); + + /* scrollbar for diff text */ + bounds.right = browser->win->portRect.right - padding; + bounds.left = bounds.right - SCROLLBAR_WIDTH; + bounds.bottom++; + bounds.top--; + browser->diff_scroller = NewControl(browser->win, &bounds, "\p", true, + 1, 1, 1, scrollBarProc, 0L); + + browser_update_menu(browser); + DrawMenuBar(); + browser_add_files(browser); + UpdateScrollbarForTE(browser->diff_scroller, browser->diff_te); + ShowWindow(browser->win); + + return browser; +} + +void +browser_close(struct browser *browser) +{ + if (browser->committer) + browser_close_committer(browser); + + if (browser->repo) + repo_close(browser->repo); + + DisposeWindow(browser->win); + DisposHandle(browser->diff_button); + DisposHandle(browser->diff_scroller); + TEDispose(browser->diff_te); + DisposHandle(browser->commit_list); + DisposHandle(browser->file_list); + + free(browser); +} + +void +browser_close_committer(struct browser *browser) +{ + if (browser->committer) { + committer_close(browser->committer); + browser->committer = NULL; + } + browser->state = BROWSER_STATE_IDLE; + SetPort(browser->win); +} + +void +browser_add_files(struct browser *browser) +{ + Cell cell = { 0, 0 }; + short i; + + LDoDraw(false, browser->file_list); + LDelRow(0, 0, browser->file_list); + browser_show_commit(browser, NULL); + + TextFont(applFont); + TextSize(10); + LAddRow(1, cell.v, browser->file_list); + LSetCell("[All Files]", 11, cell, browser->file_list); + LSetSelect(true, cell, browser->file_list); + cell.v++; + + /* fill in files */ + for (i = 0; i < browser->repo->nfiles; i++) { + LAddRow(1, cell.v, browser->file_list); + LSetCell(&browser->repo->files[i]->filename, + strlen(browser->repo->files[i]->filename), cell, + browser->file_list); + cell.v++; + } + + LDoDraw(true, browser->file_list); + + browser_filter_commits(browser); + InvalRect(&browser->win->portRect); +} + +short +browser_selected_file_ids(struct browser *browser, short **selected_files) +{ + Cell cell = { 0 }; + short nselected_files = 0, i; + + if (browser->repo->nfiles == 0) { + *selected_files = NULL; + return 0; + } + + *selected_files = xmalloc(browser->repo->nfiles * sizeof(short)); + + /* + * "All Files" is always cell.v=0, so if v=0 is selected or nothing + * is, select all files + */ + if (LGetSelect(false, &cell, browser->file_list) == true || + LGetSelect(true, &cell, browser->file_list) == false) { + nselected_files = browser->repo->nfiles; + for (i = 0; i < browser->repo->nfiles; i++) + (*selected_files)[i] = browser->repo->files[i]->id; + } else { + cell.v = 0; + while (LGetSelect(true, &cell, browser->file_list)) { + (*selected_files)[nselected_files] = + browser->repo->files[cell.v - 1]->id; + nselected_files++; + LNextCell(true, true, &cell, browser->file_list); + } + } + + return nselected_files; +} + +void +browser_filter_commits(struct browser *browser) +{ + Cell cell = { 0 }; + struct repo_commit *commit; + short i, j, k, add = 0, file_id; + short *selected_files = NULL; + short nselected_files = 0; + + LDoDraw(false, browser->commit_list); + LDelRow(0, 0, browser->commit_list); + browser_show_commit(browser, NULL); + + /* fill in commits for selected files */ + nselected_files = browser_selected_file_ids(browser, &selected_files); + cell.v = 0; + for (i = 0; i < browser->repo->ncommits; i++) { + add = 0; + commit = browser->repo->commits[i]; + for (j = 0; j < commit->nfiles; j++) { + file_id = commit->file_ids[j]; + for (k = 0; k < nselected_files; k++) { + if (selected_files[k] == commit->file_ids[j]) { + add = 1; + break; + } + } + if (add) + break; + } + + if (!add) + continue; + + LAddRow(1, cell.v, browser->commit_list); + LSetCell(&(browser->repo->commits[i]), sizeof(Ptr), cell, + browser->commit_list); + cell.v++; + } + + LDoDraw(true, browser->commit_list); + InvalRect(&(*(browser->commit_list))->rView); + + if (selected_files) + free(selected_files); +} + +void +browser_show_commit(struct browser *browser, struct repo_commit *commit) +{ + struct repo_diff *diff; + + if (commit == NULL) + TESetText("", 0, browser->diff_te); + else { + SetCursor(*(GetCursor(watchCursor))); + repo_show_diff_text(commit, browser->diff_te); + SetCursor(&arrow); + } + + InvalRect(&(*(browser->diff_te))->viewRect); + UpdateScrollbarForTE(browser->diff_scroller, browser->diff_te); +} + +void +browser_update_menu(struct browser *browser) +{ + size_t vlines; + TERec *diff; + + HLock(browser->diff_te); + diff = *(browser->diff_te); + + if (diff->nLines == 0) + DisableItem(edit_menu, EDIT_MENU_SELECT_ALL_ID); + else + EnableItem(edit_menu, EDIT_MENU_SELECT_ALL_ID); + + if (diff->selStart == diff->selEnd) + DisableItem(edit_menu, EDIT_MENU_COPY_ID); + else + EnableItem(edit_menu, EDIT_MENU_COPY_ID); + + if (browser->repo->nfiles == 0) + HiliteControl(browser->diff_button, 255); + else + HiliteControl(browser->diff_button, 0); + + HUnlock(browser->diff_te); + + EnableItem(repo_menu, 0); +} + +void +browser_update(struct browser *browser, EventRecord *event) +{ + Str255 buf; + Rect r; + short what = -1; + + if (event != NULL) + what = event->what; + + switch (what) { + case -1: + case updateEvt: + FillRect(&browser->win->portRect, fill_pattern); + + TextFont(monaco); + r = (*(browser->diff_te))->viewRect; + FillRect(&r, white); + TEUpdate(&r, browser->diff_te); + InsetRect(&r, -1, -1); + FrameRect(&r); + + r = (*(browser->file_list))->rView; + InsetRect(&r, -1, -1); + FillRect(&r, white); + FrameRect(&r); + TextFont(applFont); + TextSize(10); + LUpdate(browser->win->visRgn, browser->file_list); + + r = (*(browser->commit_list))->rView; + InsetRect(&r, -1, -1); + FillRect(&r, white); + FrameRect(&r); + LUpdate(browser->win->visRgn, browser->commit_list); + + TextFont(applFont); + TextSize(11); + browser_update_menu(browser); + UpdtControl(browser->win, browser->win->visRgn); + + break; + case activateEvt: + if (event->modifiers & activeFlag) { + TEActivate(browser->diff_te); + } else { + TEDeactivate(browser->diff_te); + } + break; + } +} + +void +browser_mouse_down(struct browser *browser, EventRecord *event) +{ + Cell selected = { 0 }, now = { 0 }, t = { 0 }; + Point p; + ControlHandle control; + Rect r; + short *selected_files = NULL, *now_selected_files = NULL; + short nselected = 0, nnow_selected = 0; + short val, adj, page, was_selected, part, i; + char *path; + + p = event->where; + GlobalToLocal(&p); + + /* is it in diff text? */ + r = (*(browser->diff_te))->viewRect; + if (PtInRect(p, &r)) { + TEClick(p, ((event->modifiers & shiftKey) != 0), browser->diff_te); + browser_update_menu(browser); + return; + } + + /* is it in file list? */ + r = (*(browser->file_list))->rView; + r.right += SCROLLBAR_WIDTH; + if (PtInRect(p, &r)) { + /* store what is selected now */ + nselected = browser_selected_file_ids(browser, &selected_files); + + /* in case any new items are redrawn in LClick */ + TextFont(applFont); + TextSize(10); + + /* possibly highlight a new cell */ + LClick(p, event->modifiers, browser->file_list); + + nnow_selected = browser_selected_file_ids(browser, + &now_selected_files); + + if (nnow_selected == 0) { + /* can't select nothing, select 'all files' */ + selected.v = 0; + LSetSelect(true, selected, browser->file_list); + if (nselected != browser->repo->nfiles) + browser_filter_commits(browser); + } else if (nselected != nnow_selected) { + browser_filter_commits(browser); + } else { + for (i = 0; i < nselected; i++) { + if (selected_files[i] != now_selected_files[i]) { + browser_filter_commits(browser); + break; + } + } + } + + if (selected_files) + free(selected_files); + if (now_selected_files) + free(now_selected_files); + + return; + } + + /* is it in commit list? */ + r = (*(browser->commit_list))->rView; + r.right += SCROLLBAR_WIDTH; + if (PtInRect(p, &r)) { + if (browser->repo == NULL) + return; + + /* store what is selected now */ + was_selected = LGetSelect(true, &selected, browser->commit_list); + + /* possibly highlight a new cell */ + LClick(p, event->modifiers, browser->commit_list); + + if (LGetSelect(true, &now, browser->commit_list) == false) { + if (was_selected) + browser_show_commit(browser, NULL); + } else if (!was_selected || selected.v != now.v) { + if (was_selected) + LSetSelect(false, selected, browser->commit_list); + LSetSelect(true, now, browser->commit_list); + browser_show_commit(browser, browser->repo->commits[now.v]); + } + + return; + } + + switch (part = FindControl(p, browser->win, &control)) { + case inButton: + TextFont(applFont); + TextSize(11); + if (TrackControl(control, p, 0L) && + control == browser->diff_button) + browser->state = BROWSER_STATE_OPEN_COMMITTER; + break; + case inUpButton: + case inDownButton: + case inPageUp: + case inPageDown: + if (control == browser->diff_scroller) + SetTrackControlTE(browser->diff_te); + else + break; + TrackControl(control, p, TrackMouseDownInControl); + break; + case inThumb: + val = GetCtlValue(control); + if (TrackControl(control, p, 0L) == 0) + break; + adj = val - GetCtlValue(control); + if (adj != 0) { + val -= adj; + if (control == browser->diff_scroller) + TEScroll(0, adj * (*(browser->diff_te))->lineHeight, + browser->diff_te); + SetCtlValue(control, val); + } + break; + } +} --- browser.h Mon Oct 18 13:09:23 2021 +++ browser.h Mon Oct 18 13:09:23 2021 @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef __BROWSER_H__ +#define __BROWSER_H__ + +#include <stdlib.h> + +#include "committer.h" +#include "repo.h" + +enum { + BROWSER_STATE_IDLE, + BROWSER_STATE_ADD_FILE, + BROWSER_STATE_UPDATE_FILE_LIST, + BROWSER_STATE_UPDATE_COMMIT_LIST, + BROWSER_STATE_OPEN_COMMITTER, + BROWSER_STATE_COMMITTER_DO_DIFF, + BROWSER_STATE_WAITING_FOR_COMMITTER +}; + +struct browser { + short state; + WindowPtr win; + struct repo *repo; + ListHandle file_list; + ListHandle commit_list; + TEHandle diff_te; + ControlHandle diff_scroller; + ControlHandle diff_button; + struct committer *committer; +}; + +struct browser *browser_init(struct repo *repo); +void browser_close(struct browser *browser); +void browser_update_titlebar(struct browser *browser); +void browser_close(struct browser *browser); +void browser_close_committer(struct browser *browser); +void browser_idle(struct browser *browser); +void browser_update_menu(struct browser *browser); +void browser_update(struct browser *browser, EventRecord *event); +void browser_show_commit(struct browser *browser, + struct repo_commit *commit); +short browser_selected_file_ids(struct browser *browser, + short **selected_files); +void browser_mouse_down(struct browser *browser, EventRecord *event); + +pascal void commit_list_ldef(short message, Boolean selected, + Rect *cellRect, Cell theCell, short dataOffset, short dataLen, + ListHandle theList); + +#endif --- committer.c Mon Oct 18 16:47:01 2021 +++ committer.c Tue Oct 19 12:58:34 2021 @@ -0,0 +1,553 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdarg.h> +#include <stdio.h> +#include <string.h> + +#include "amend.h" +#include "browser.h" +#include "committer.h" +#include "diff.h" +#include "repo.h" +#include "util.h" + +/* needed by diffreg */ +struct stat stb1, stb2; +long diff_format, diff_context, status = 0; +char *ifdefname, *diffargs, *label[2], *ignore_pats; + +#define DIFF_LINE_SIZE 1024 +Handle diff_line = NULL; +size_t diff_line_pos = 0; + +static short padding = 10; +static short diff_too_big = 0; + +DialogPtr committer_dialog = NULL; + +/* so diff_* know what it's operating on */ +struct committer *cur_committer = NULL; + +void committer_generate_diff(struct committer *committer); +void committer_update_menu(struct committer *committer); +void committer_status(char *format, ...); +void committer_commit(struct committer *committer); +void diff_append_line(char *str, size_t len); +void diff_finish(void); + +void +committer_init(struct browser *browser) +{ + char title[256] = { 0 }; + struct committer *committer; + Rect bounds = { 0 }, te_bounds = { 0 }; + short fh; + + committer = xmalloczero(sizeof(struct committer)); + committer->browser = browser; + browser->committer = committer; + + /* main window */ + bounds.left = (padding / 2); + bounds.top = screenBits.bounds.top + (GetMBarHeight() * 2) - 1 + + (padding / 2); + bounds.right = screenBits.bounds.right - 1 - (padding / 2); + bounds.bottom = screenBits.bounds.bottom - 1 - (padding / 2); + + if (sprintf(title, "%s: %s: diff", PROGRAM_NAME, (browser->repo ? + browser->repo->filename : "No repo open")) > sizeof(title)) + err(1, "sprintf overflow!"); + + committer->win = NewWindow(0L, &bounds, CtoPstr(title), false, + noGrowDocProc, (WindowPtr)-1L, true, 0); + if (!committer) + err(1, "Can't create committer window"); + SetPort(committer->win); + + /* log message */ + bounds.top = padding; + bounds.left = 50; + fh = FontHeight(monaco, 9); + bounds.bottom = bounds.top + 2 + (fh * 5) + 2; + bounds.right = committer->win->portRect.right - SCROLLBAR_WIDTH - + padding; + TextFont(monaco); + TextSize(9); + te_bounds = bounds; + InsetRect(&te_bounds, 2, 2); + committer->log_te = TENew(&te_bounds, &bounds); + + /* scrollbar for log message */ + bounds.left = bounds.right; + bounds.right += SCROLLBAR_WIDTH; + bounds.bottom++; + bounds.top--; + committer->log_scroller = NewControl(committer->win, &bounds, "\p", + true, 1, 1, 1, scrollBarProc, 0L); + + /* diff */ + bounds.top = bounds.bottom + padding; + bounds.left = padding; + bounds.bottom = committer->win->portRect.bottom - 20 - padding - + padding; + bounds.right = committer->win->portRect.right - SCROLLBAR_WIDTH - + padding; + TextFont(monaco); + TextSize(9); + te_bounds = bounds; + InsetRect(&te_bounds, 2, 2); + committer->diff_te = TENew(&te_bounds, &bounds); + + /* scrollbar for diff */ + bounds.left = bounds.right; + bounds.right += SCROLLBAR_WIDTH; + bounds.bottom++; + bounds.top--; + committer->diff_scroller = NewControl(committer->win, &bounds, "\p", + true, 1, 1, 1, scrollBarProc, 0L); + + /* commit button */ + TextFont(applFont); + TextSize(11); + bounds.left = committer->win->portRect.right - padding - 100; + bounds.right = bounds.left + 100; + bounds.bottom = committer->win->portRect.bottom - padding; + bounds.top = bounds.bottom - 20; + committer->commit_button = NewControl(committer->win, &bounds, + "\pCommit", true, 1, 1, 1, pushButProc, 0L); + + ShowWindow(committer->win); + committer_update_menu(committer); + DrawMenuBar(); + + committer_update(committer, NULL); +} + +void +committer_close(struct committer *committer) +{ + if (diff_line != NULL) { + DisposHandle(diff_line); + diff_line = NULL; + } + + DisposeWindow(committer->win); + TEDispose(committer->log_te); + DisposHandle(committer->log_scroller); + TEDispose(committer->diff_te); + DisposHandle(committer->diff_scroller); + DisposHandle(committer->commit_button); + + free(committer); +} + +void +committer_idle(struct committer *committer) +{ + TEIdle(committer->log_te); +} + +void +committer_update(struct committer *committer, EventRecord *event) +{ + Str255 buf; + Rect r; + short what = -1, len; + + if (event != NULL) + what = event->what; + + switch (what) { + case -1: + case updateEvt: + TextFont(applFont); + TextSize(11); + + r = (*(committer->log_te))->viewRect; + MoveTo(r.top, r.top + FontHeight(applFont, 10) - 2); + DrawText("Log:", 0, 4); + + TextFont(monaco); + r = (*(committer->log_te))->viewRect; + TEUpdate(&r, committer->log_te); + InsetRect(&r, -1, -1); + FrameRect(&r); + + r = (*(committer->diff_te))->viewRect; + TEUpdate(&r, committer->diff_te); + InsetRect(&r, -1, -1); + FrameRect(&r); + + if ((*(committer->diff_te))->nLines > 0) { + r = (*(committer->diff_te))->viewRect; + MoveTo(r.left, r.bottom + FontHeight(applFont, 10) + padding); + len = sprintf((char *)buf, "%d (+), %d (-)", + committer->diff_adds, committer->diff_subs); + DrawText(buf, 0, len); + } + + committer_update_menu(committer); + UpdtControl(committer->win, committer->win->visRgn); + + break; + case activateEvt: + if (event->modifiers & activeFlag) { + TEActivate(committer->log_te); + TEActivate(committer->diff_te); + } else { + TEDeactivate(committer->log_te); + TEDeactivate(committer->diff_te); + } + break; + } +} + +void +committer_key_down(struct committer *committer, EventRecord *event) +{ + char k; + + k = (event->message & charCodeMask); + TEKey(k, committer->log_te); + UpdateScrollbarForTE(committer->log_scroller, committer->log_te); + committer_update_menu(committer); +} + +void +committer_mouse_down(struct committer *committer, EventRecord *event) +{ + Point p; + ControlHandle control; + Rect r; + short val, adj, page, was_selected, part, i; + + p = event->where; + GlobalToLocal(&p); + + r = (*(committer->diff_te))->viewRect; + if (PtInRect(p, &r)) { + TEClick(p, ((event->modifiers & shiftKey) != 0), + committer->diff_te); + committer_update_menu(committer); + return; + } + + r = (*(committer->log_te))->viewRect; + if (PtInRect(p, &r)) { + TEClick(p, ((event->modifiers & shiftKey) != 0), + committer->log_te); + committer_update_menu(committer); + return; + } + + switch (part = FindControl(p, committer->win, &control)) { + case inButton: + TextFont(applFont); + TextSize(11); + if (TrackControl(control, p, 0L) && + control == committer->commit_button) + committer_commit(committer); + break; + case inUpButton: + case inDownButton: + case inPageUp: + case inPageDown: + if (control == committer->diff_scroller) + SetTrackControlTE(committer->diff_te); + else if (control == committer->log_scroller) + SetTrackControlTE(committer->log_te); + else + break; + TrackControl(control, p, TrackMouseDownInControl); + break; + case inThumb: + val = GetCtlValue(control); + if (TrackControl(control, p, 0L) == 0) + break; + adj = val - GetCtlValue(control); + if (adj != 0) { + val -= adj; + if (control == committer->diff_scroller) + TEScroll(0, adj * (*(committer->diff_te))->lineHeight, + committer->diff_te); + else if (control == committer->log_scroller) + TEScroll(0, adj * (*(committer->log_te))->lineHeight, + committer->log_te); + SetCtlValue(control, val); + } + break; + } +} + +void +committer_update_menu(struct committer *committer) +{ + HLock(committer->diff_te); + HLock(committer->log_te); + + if ((*(committer->diff_te))->nLines == 0 && + (*(committer->log_te))->nLines == 0) { + DisableItem(edit_menu, EDIT_MENU_SELECT_ALL_ID); + HiliteControl(committer->commit_button, 255); + } else { + EnableItem(edit_menu, EDIT_MENU_SELECT_ALL_ID); + if ((*(committer->log_te))->nLines > 0) + HiliteControl(committer->commit_button, 0); + } + + if ((*(committer->diff_te))->selStart == + (*(committer->diff_te))->selEnd && + (*(committer->log_te))->selStart == + (*(committer->log_te))->selEnd) + DisableItem(edit_menu, EDIT_MENU_COPY_ID); + else + EnableItem(edit_menu, EDIT_MENU_COPY_ID); + + HUnlock(committer->log_te); + HUnlock(committer->diff_te); + + EnableItem(repo_menu, 0); +} + +void +committer_generate_diff(struct committer *committer) +{ + struct repo_file *file; + short i; + short *selected_files = NULL; + short nselected_files = 0; + + SetCursor(*(GetCursor(watchCursor))); + + nselected_files = browser_selected_file_ids(committer->browser, + &selected_files); + + /* default to unified diffs (should this be a setting?) */ + diff_format = D_UNIFIED; + diff_context = 3; + + committer->diff_adds = 0; + committer->diff_subs = 0; + + cur_committer = committer; + diff_too_big = 0; + + HLock(committer->diff_te); + + for (i = 0; i < nselected_files; i++) { + file = repo_file_with_id(committer->browser->repo, + selected_files[i]); + if (file == NULL) + err(1, "Failed to find file with id %d", selected_files[i]); + committer_status("Diffing %s...", file->filename); + repo_diff_file(committer->browser->repo, file); + diff_finish(); + } + + HUnlock(committer->diff_te); + InvalRect(&committer->win->portRect); + UpdateScrollbarForTE(committer->diff_scroller, committer->diff_te); + cur_committer = NULL; + + committer_status(NULL); + +done_diffing: + if (selected_files != NULL) + free(selected_files); + SetCursor(&arrow); +} + +void +committer_status(char *format, ...) +{ + Handle thandle; + va_list argptr; + Str255 status; + Rect trect; + size_t len, last_pos, last_line, i; + short ttype; + + if (format == NULL) { + DisposDialog(committer_dialog); + committer_dialog = NULL; + return; + } + + va_start(argptr, format); + len = vsprintf((char *)status, format, argptr); + if (len > sizeof(status)) + err(1, "vsprintf overflow!"); + va_end(argptr); + CtoPstr(status); + + if (committer_dialog == NULL) + committer_dialog = GetNewDialog(WAIT_DLOG_ID, NULL, (WindowPtr)-1); + + GetDItem(committer_dialog, 2, &ttype, &thandle, &trect); + SetIText(thandle, status); +} + +void +committer_commit(struct committer *committer) +{ + struct browser *browser; + short loglen; + short *selected_files = NULL; + short nselected_files = 0; + + HLock(committer->log_te); + HLock(committer->diff_te); + + loglen = (*(committer->log_te))->teLength; + + nselected_files = browser_selected_file_ids(committer->browser, + &selected_files); + + SetCursor(*(GetCursor(watchCursor))); + committer_status("Committing changes..."); + + repo_commit(committer->browser->repo, selected_files, nselected_files, + committer->diff_adds, committer->diff_subs, + (*(committer->log_te))->hText, loglen, + (*(committer->diff_te))->hText, committer->diff_te_len); + + HUnlock(committer->diff_te); + HUnlock(committer->log_te); + + committer_status(NULL); + SetCursor(&arrow); + + browser = committer->browser; + browser_close_committer(committer->browser); + browser->state = BROWSER_STATE_UPDATE_COMMIT_LIST; +} + +size_t +diff_output(const char *format, ...) +{ + va_list argptr; + size_t len, last_pos, last_line, i; + + if (diff_line == NULL) { + diff_line = xNewHandle(DIFF_LINE_SIZE); + diff_line_pos = 0; + HLock(diff_line); + } + + last_pos = diff_line_pos; + last_line = 0; + + va_start(argptr, format); + + if (format[0] == '%' && format[1] == 'c' && format[2] == '\0') { + /* avoid having to vsprintf just to append 1 character */ + (*diff_line)[last_pos] = va_arg(argptr, int); + len = 1; + } else + len = vsprintf(*diff_line + last_pos, format, argptr); + + va_end(argptr); + + diff_line_pos += len; + if (diff_line_pos >= DIFF_LINE_SIZE) + err(1, "diff line overflow!"); + + if (len == 1 && (*diff_line)[last_pos] != '\n') + return 1; + + for (i = last_pos; i < diff_line_pos; i++) { + if (((char *)*diff_line)[i] == '\n') { + ((char *)*diff_line)[i] = '\r'; + diff_append_line(*diff_line + last_line, i - last_line + 1); + last_line = i + 1; + } + } + + if (last_line == diff_line_pos) { + diff_line_pos = 0; + } else if (last_line > 0) { + memmove(*diff_line, *diff_line + last_line, + diff_line_pos - last_line); + diff_line_pos -= last_line; + } + + return len; +} + +void +diff_append_line(char *str, size_t len) +{ + unsigned long cur_htext_size; + short tabsize; + + cur_htext_size = GetHandleSize((*(cur_committer->diff_te))->hText); + if (cur_committer->diff_te_len + len >= cur_htext_size) { + SetHandleSize((*(cur_committer->diff_te))->hText, + cur_htext_size + (1024 * 4)); + if (MemError()) + err(1, "Out of memory! Can't expand diff TE beyond %lu bytes.", + cur_htext_size); + } + + HLock((*(cur_committer->diff_te))->hText); + memcpy(*(*(cur_committer->diff_te))->hText + + cur_committer->diff_te_len, str, len); + HUnlock((*(cur_committer->diff_te))->hText); + + cur_committer->diff_te_len += len; + + if (!diff_too_big && TECanAddLine(cur_committer->diff_te, len)) { + (*(cur_committer->diff_te))->teLength = cur_committer->diff_te_len; + } else { + diff_too_big = 1; + } + + if (str[0] == '-' && str[1] != '-') + cur_committer->diff_subs++; + else if (str[0] == '+' && str[1] != '+') + cur_committer->diff_adds++; +} + +void +diff_finish(void) +{ + if (diff_line != NULL) { + if (diff_line_pos) + diff_append_line(*diff_line, diff_line_pos); + + DisposHandle(diff_line); + diff_line = NULL; + } + +#if 0 + if (diff_chunk != NULL) { + if (diff_chunk_pos) { + TESetSelect(1024 * 32, 1024 * 32, cur_committer->diff_te); + TEInsert((*diff_chunk), diff_chunk_pos, + cur_committer->diff_te); + } + + diff_chunk_pos = 0; + DisposHandle(diff_chunk); + diff_chunk = NULL; + } +#endif + + SetHandleSize((*(cur_committer->diff_te))->hText, + cur_committer->diff_te_len); + TECalText(cur_committer->diff_te); +} --- committer.h Mon Oct 18 15:49:37 2021 +++ committer.h Tue Oct 19 11:43:19 2021 @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef __COMMITTER_H__ +#define __COMMITTER_H__ + +#include "browser.h" + +#define WAIT_DLOG_ID 128 + +struct committer { + struct browser *browser; + WindowPtr win; + TEHandle log_te; + ControlHandle log_scroller; + TEHandle diff_te; + unsigned long diff_te_len; + ControlHandle diff_scroller; + ControlHandle commit_button; + short diff_adds; + short diff_subs; +}; + +void committer_init(struct browser *browser); +void committer_close(struct committer *committer); +void committer_idle(struct committer *committer); +void committer_update(struct committer *committer, EventRecord *event); +void committer_key_down(struct committer *committer, EventRecord *event); +void committer_mouse_down(struct committer *committer, EventRecord *event); +void committer_generate_diff(struct committer *committer); + +size_t diff_output(const char *format, ...); + +#endif --- commit_list.c Mon Oct 18 13:13:50 2021 +++ commit_list.c Mon Oct 18 13:13:50 2021 @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <string.h> +#include <stdio.h> +#include <time.h> +#include "browser.h" +#include "repo.h" +#include "util.h" + +void commit_list_draw_cell(ListHandle theList, Cell theCell, + short dataLen, Rect *cellRect, Boolean selected); + +pascal void +commit_list_ldef(short message, Boolean selected, Rect *cellRect, + Cell theCell, short dataOffset, short dataLen, ListHandle theList) +{ + short id; + + switch (message) { + case 0: + /* init */ + break; + case 1: + /* update */ + /* FALLTHROUGH */ + case 2: + /* hilight */ + commit_list_draw_cell(theList, theCell, dataLen, cellRect, + selected); + break; + case 3: + /* close */ + break; + } +} + +void +commit_list_draw_cell(ListHandle theList, Cell theCell, short dataLen, + Rect *cellRect, Boolean selected) +{ + RgnHandle savedClip; + PenState savedPenState; + GrafPtr savedPort; + Rect textRect; + char tmp[50]; + struct repo_commit *commit = NULL; + short offset, len, height; + struct tm *ttm = NULL; + + LGetCell(&commit, &dataLen, theCell, theList); + if (commit == NULL) + /* XXX: why does this happen? */ + return; + + textRect.left = cellRect->left; + textRect.right = cellRect->right; + textRect.top = cellRect->top; + textRect.bottom = cellRect->bottom + 1; + EraseRect(&textRect); + InsetRect(&textRect, 4, 4); + + height = FontHeight(applFont, 9); + MoveTo(textRect.left, textRect.bottom - height); + TextFace(bold); + TextFont(applFont); + TextSize(9); + + ttm = localtime(&commit->date); + sprintf(tmp, "%04d-%02d-%02d %02d:%02d:%02d", ttm->tm_year + 1900, + ttm->tm_mon + 1, ttm->tm_mday, ttm->tm_hour, ttm->tm_min, + ttm->tm_sec); + DrawText(tmp, 0, strlen(tmp)); + + MoveTo(textRect.left + 140, textRect.bottom - height); + DrawText(commit->author, 0, strlen(commit->author)); + + MoveTo(textRect.left, textRect.bottom - 1); + HLock(commit->log); + TextFace(normal); + DrawText(*(commit->log), 0, commit->log_len); + HUnlock(commit->log); + + if (selected) + InvertRect(cellRect); +} --- diff.h Fri Oct 15 17:37:12 2021 +++ diff.h Fri Oct 15 17:37:12 2021 @@ -0,0 +1,87 @@ +/* $OpenBSD: diff.h,v 1.34 2020/11/01 18:16:08 jcs Exp $ */ + +/* + * Copyright (c) 1991, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * @(#)diff.h 8.1 (Berkeley) 6/6/93 + */ + +#include <stddef.h> + +/* + * Output format options + */ +#define D_NORMAL 0 /* Normal output */ +#define D_EDIT -1 /* Editor script out */ +#define D_REVERSE 1 /* Reverse editor script */ +#define D_CONTEXT 2 /* Diff with context */ +#define D_UNIFIED 3 /* Unified context diff */ +#define D_IFDEF 4 /* Diff with merged #ifdef's */ +#define D_NREVERSE 5 /* Reverse ed script with numbered + lines and no trailing . */ +#define D_BRIEF 6 /* Say if the files differ */ + +/* + * Output flags + */ +#define D_HEADER 0x001 /* Print a header/footer between files */ +#define D_EMPTY1 0x002 /* Treat first file as empty (/dev/null) */ +#define D_EMPTY2 0x004 /* Treat second file as empty (/dev/null) */ + +/* + * Command line flags + */ +#define D_FORCEASCII 0x008 /* Treat file as ascii regardless of content */ +#define D_FOLDBLANKS 0x010 /* Treat all white space as equal */ +#define D_MINIMAL 0x020 /* Make diff as small as possible */ +#define D_IGNORECASE 0x040 /* Case-insensitive matching */ +#define D_PROTOTYPE 0x080 /* Display C function prototype */ +#define D_EXPANDTABS 0x100 /* Expand tabs to spaces */ +#define D_IGNOREBLANKS 0x200 /* Ignore white space changes */ + +/* + * Status values for print_status() and diffreg() return values + */ +#define D_SAME 0 /* Files are the same */ +#define D_DIFFER 1 /* Files are different */ +#define D_BINARY 2 /* Binary files are different */ +#define D_MISMATCH1 3 /* path1 was a dir, path2 a file */ +#define D_MISMATCH2 4 /* path1 was a file, path2 a dir */ +#define D_SKIPPED1 5 /* path1 was a special file */ +#define D_SKIPPED2 6 /* path2 was a special file */ + +extern short quitting; +extern long diff_format, diff_context, status; +extern char *ifdefname, *diffargs, *label[2], *ignore_pats; +extern struct stat stb1, stb2; + +char *splice(char *, char *); +int diffreg(char *, char *, int); +void diffdir(char *, char *, int); + +size_t diff_output(const char *, ...); --- diffreg.c Sun Oct 17 14:48:37 2021 +++ diffreg.c Sun Oct 17 14:48:37 2021 @@ -0,0 +1,1410 @@ +/* $OpenBSD: diffreg.c,v 1.93 2019/06/28 13:35:00 deraadt Exp $ */ + +/* + * Copyright (C) Caldera International Inc. 2001-2002. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code and documentation must retain the above + * copyright notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed or owned by Caldera + * International, Inc. + * 4. Neither the name of Caldera International, Inc. nor the names of other + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * USE OF THE SOFTWARE PROVIDED FOR UNDER THIS LICENSE BY CALDERA + * INTERNATIONAL, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL CALDERA INTERNATIONAL, INC. BE LIABLE FOR ANY DIRECT, + * INDIRECT INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/*- + * Copyright (c) 1991, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * @(#)diffreg.c 8.1 (Berkeley) 6/6/93 + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <unix.h> + +#include "diff.h" +#include "util.h" + +#define MINIMUM(a, b) (((a) < (b)) ? (a) : (b)) +#define MAXIMUM(a, b) (((a) > (b)) ? (a) : (b)) + +/* + * diff - compare two files. + */ + +/* + * Uses an algorithm due to Harold Stone, which finds + * a pair of longest identical subsequences in the two + * files. + * + * The major goal is to generate the match vector J. + * J[i] is the index of the line in file1 corresponding + * to line i file0. J[i] = 0 if there is no + * such line in file1. + * + * Lines are hashed so as to work in core. All potential + * matches are located by sorting the lines of each file + * on the hash (called ``value''). In particular, this + * collects the equivalence classes in file1 together. + * Subroutine equiv replaces the value of each line in + * file0 by the index of the first element of its + * matching equivalence in (the reordered) file1. + * To save space equiv squeezes file1 into a single + * array member in which the equivalence classes + * are simply concatenated, except that their first + * members are flagged by changing sign. + * + * Next the indices that point into member are unsorted into + * array class according to the original order of file0. + * + * The cleverness lies in routine stone. This marches + * through the lines of file0, developing a vector klist + * of "k-candidates". At step i a k-candidate is a matched + * pair of lines x,y (x in file0 y in file1) such that + * there is a common subsequence of length k + * between the first i lines of file0 and the first y + * lines of file1, but there is no such subsequence for + * any smaller y. x is the earliest possible mate to y + * that occurs in such a subsequence. + * + * Whenever any of the members of the equivalence class of + * lines in file1 matable to a line in file0 has serial number + * less than the y of some k-candidate, that k-candidate + * with the smallest such y is replaced. The new + * k-candidate is chained (via pred) to the current + * k-1 candidate so that the actual subsequence can + * be recovered. When a member has serial number greater + * that the y of all k-candidates, the klist is extended. + * At the end, the longest subsequence is pulled out + * and placed in the array J by unravel + * + * With J in hand, the matches there recorded are + * check'ed against reality to assure that no spurious + * matches have crept in due to hashing. If they have, + * they are broken, and "jackpot" is recorded--a harmless + * matter except that a true match for a spuriously + * mated line may now be unnecessarily reported as a change. + * + * Much of the complexity of the program comes simply + * from trying to minimize core utilization and + * maximize the range of doable problems by dynamically + * allocating what is needed and reusing what is not. + * The core requirements for problems larger than somewhat + * are (in words) 2*length(file0) + length(file1) + + * 3*(number of k-candidates installed), typically about + * 6n words for files of length n. + */ + +struct cand { + int x; + int y; + int pred; +}; + +struct line { + int serial; + int value; +} *file[2]; + +/* + * The following struct is used to record change information when + * doing a "context" or "unified" diff. (see routine "change" to + * understand the highly mnemonic field names) + */ +struct context_vec { + int a; /* start line in old file */ + int b; /* end line in old file */ + int c; /* start line in new file */ + int d; /* end line in new file */ +}; + +static void output(char *, FILE *, char *, FILE *, int); +static void check(FILE *, FILE *, int); +static void range(int, int, char *); +static void uni_range(int, int); +static void dump_context_vec(FILE *, FILE *, int); +static void dump_unified_vec(FILE *, FILE *, int); +static void prepare(int, FILE *, off_t, int); +static void prune(void); +static void equiv(struct line *, int, struct line *, int, int *); +static void unravel(int); +static void unsort(struct line *, int, int *); +static void change(char *, FILE *, char *, FILE *, int, int, int, int, int *); +static void sort(struct line *, int); +static void print_header(const char *, const char *); +static int ignoreline(char *); +static int asciifile(FILE *); +static int fetch(long *, int, int, FILE *, int, int, int); +static int newcand(int, int, int); +static int search(int *, int, int); +static int skipline(FILE *); +static int isqrt(int); +static int stone(int *, int, int *, int *, int); +static int readhash(FILE *, int); +static int files_differ(FILE *, FILE *, int); +static char *match_function(const long *, int, FILE *); +static char *preadline(int, size_t, off_t); + +static int *J; /* will be overlaid on class */ +static int *class; /* will be overlaid on file[0] */ +static int *klist; /* will be overlaid on file[0] after class */ +static int *member; /* will be overlaid on file[1] */ +static int clen; +static int inifdef; /* whether or not we are in a #ifdef block */ +static int len[2]; +static int pref, suff; /* length of prefix and suffix */ +static int slen[2]; +static int anychange; +static long *ixnew; /* will be overlaid on file[1] */ +static long *ixold; /* will be overlaid on klist */ +static struct cand *clist; /* merely a free storage pot for candidates */ +static int clistlen; /* the length of clist */ +static struct line *sfile[2]; /* shortened by pruning common prefix/suffix */ +static u_char *chrtran; /* translation table for case-folding */ +static struct context_vec *context_vec_start; +static struct context_vec *context_vec_end; +static struct context_vec *context_vec_ptr; + +#define FUNCTION_CONTEXT_SIZE 55 +static char lastbuf[FUNCTION_CONTEXT_SIZE]; +static int lastline; +static int lastmatchline; + + +/* + * chrtran points to one of 2 translation tables: cup2low if folding upper to + * lower case clow2low if not folding case + */ +u_char clow2low[256] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, + 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, + 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, + 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, + 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, + 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, + 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, + 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, + 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, + 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, + 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, + 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, + 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, + 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, + 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, + 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, + 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, + 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, + 0xfd, 0xfe, 0xff +}; + +u_char cup2low[256] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, + 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, + 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, + 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x60, 0x61, + 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, + 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x60, 0x61, 0x62, + 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, + 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, + 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, + 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, + 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, + 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, + 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, + 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, + 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, + 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, + 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, + 0xfd, 0xfe, 0xff +}; + +int +diffreg(char *file1, char *file2, int flags) +{ + FILE *f1, *f2; + int i, rval; + + f1 = f2 = NULL; + rval = D_SAME; + anychange = 0; + lastline = 0; + lastmatchline = 0; + context_vec_ptr = context_vec_start - 1; + if (flags & D_IGNORECASE) + chrtran = cup2low; + else + chrtran = clow2low; + if (strcmp(file1, "-") == 0 && strcmp(file2, "-") == 0) + goto closem; + + f1 = fopen(file1, "r"); + if (f1 == NULL) { + warn("%s", file1); + status |= 2; + goto closem; + } + + f2 = fopen(file2, "r"); + if (f2 == NULL) { + warn("%s", file2); + status |= 2; + goto closem; + } + + switch (files_differ(f1, f2, flags)) { + case 0: + goto closem; + case 1: + break; + default: + /* error */ + status |= 2; + goto closem; + } + + if ((flags & D_FORCEASCII) == 0 && + (!asciifile(f1) || !asciifile(f2))) { + rval = D_BINARY; + status |= 1; + goto closem; + } + prepare(0, f1, stb1.st_size, flags); + prepare(1, f2, stb2.st_size, flags); + + prune(); + sort(sfile[0], slen[0]); + sort(sfile[1], slen[1]); + + member = (int *)file[1]; + equiv(sfile[0], slen[0], sfile[1], slen[1], member); + member = xreallocarray(member, slen[1] + 2, sizeof(*member)); + + class = (int *)file[0]; + unsort(sfile[0], slen[0], class); + class = xreallocarray(class, slen[0] + 2, sizeof(*class)); + + klist = xcalloc(slen[0] + 2, sizeof(*klist)); + clen = 0; + clistlen = 100; + clist = xcalloc(clistlen, sizeof(*clist)); + i = stone(class, slen[0], member, klist, flags); + free(member); + free(class); + + J = xreallocarray(J, len[0] + 2, sizeof(*J)); + unravel(klist[i]); + free(clist); + free(klist); + + ixold = xreallocarray(ixold, len[0] + 2, sizeof(*ixold)); + ixnew = xreallocarray(ixnew, len[1] + 2, sizeof(*ixnew)); + check(f1, f2, flags); + output(file1, f1, file2, f2, flags); +closem: + if (anychange) { + status |= 1; + if (rval == D_SAME) + rval = D_DIFFER; + } + if (f1 != NULL) + fclose(f1); + if (f2 != NULL) + fclose(f2); + + return (rval); +} + +/* + * Check to see if the given files differ. + * Returns 0 if they are the same, 1 if different, and -1 on error. + * XXX - could use code from cmp(1) [faster] + */ +static int +files_differ(FILE *f1, FILE *f2, int flags) +{ + char buf1[BUFSIZ], buf2[BUFSIZ]; + size_t i, j; + + if ((flags & (D_EMPTY1|D_EMPTY2)) || stb1.st_size != stb2.st_size) + return (1); + for (;;) { + i = fread(buf1, 1, sizeof(buf1), f1); + j = fread(buf2, 1, sizeof(buf2), f2); + if ((!i && ferror(f1)) || (!j && ferror(f2))) + return (-1); + if (i != j) + return (1); + if (i == 0) + return (0); + if (memcmp(buf1, buf2, i) != 0) + return (1); + } +} + +static void +prepare(int i, FILE *fd, off_t filesize, int flags) +{ + struct line *p; + int j, h; + size_t sz; + + rewind(fd); + + sz = (filesize <= SIZE_MAX ? filesize : SIZE_MAX) / 25; + if (sz < 100) + sz = 100; + + p = xcalloc(sz + 3, sizeof(*p)); + for (j = 0; (h = readhash(fd, flags));) { + if (j == sz) { + sz = sz * 3 / 2; + p = xreallocarray(p, sz + 3, sizeof(*p)); + } + p[++j].value = h; + } + len[i] = j; + file[i] = p; +} + +static void +prune(void) +{ + int i, j; + + for (pref = 0; pref < len[0] && pref < len[1] && + file[0][pref + 1].value == file[1][pref + 1].value; + pref++) + ; + for (suff = 0; suff < len[0] - pref && suff < len[1] - pref && + file[0][len[0] - suff].value == file[1][len[1] - suff].value; + suff++) + ; + for (j = 0; j < 2; j++) { + sfile[j] = file[j] + pref; + slen[j] = len[j] - pref - suff; + for (i = 0; i <= slen[j]; i++) + sfile[j][i].serial = i; + } +} + +static void +equiv(struct line *a, int n, struct line *b, int m, int *c) +{ + int i, j; + + i = j = 1; + while (i <= n && j <= m) { + if (a[i].value < b[j].value) + a[i++].value = 0; + else if (a[i].value == b[j].value) + a[i++].value = j; + else + j++; + } + while (i <= n) + a[i++].value = 0; + b[m + 1].value = 0; + j = 0; + while (++j <= m) { + c[j] = -b[j].serial; + while (b[j + 1].value == b[j].value) { + j++; + c[j] = b[j].serial; + } + } + c[j] = -1; +} + +/* Code taken from ping.c */ +static int +isqrt(int n) +{ + int y, x = 1; + + if (n == 0) + return (0); + + do { /* newton was a stinker */ + y = x; + x = n / x; + x += y; + x /= 2; + } while ((x - y) > 1 || (x - y) < -1); + + return (x); +} + +static int +stone(int *a, int n, int *b, int *c, int flags) +{ + int i, k, y, j, l; + int oldc, tc, oldl, sq; + u_int numtries, bound; + + if (flags & D_MINIMAL) + bound = UINT_MAX; + else { + sq = isqrt(n); + bound = MAXIMUM(256, sq); + } + + k = 0; + c[0] = newcand(0, 0, 0); + for (i = 1; i <= n; i++) { + j = a[i]; + if (j == 0) + continue; + y = -b[j]; + oldl = 0; + oldc = c[0]; + numtries = 0; + do { + if (y <= clist[oldc].y) + continue; + l = search(c, k, y); + if (l != oldl + 1) + oldc = c[l - 1]; + if (l <= k) { + if (clist[c[l]].y <= y) + continue; + tc = c[l]; + c[l] = newcand(i, y, oldc); + oldc = tc; + oldl = l; + numtries++; + } else { + c[l] = newcand(i, y, oldc); + k++; + break; + } + } while ((y = b[++j]) > 0 && numtries < bound); + } + return (k); +} + +static int +newcand(int x, int y, int pred) +{ + struct cand *q; + + if (clen == clistlen) { + clistlen = clistlen * 11 / 10; + clist = xreallocarray(clist, clistlen, sizeof(*clist)); + } + q = clist + clen; + q->x = x; + q->y = y; + q->pred = pred; + return (clen++); +} + +static int +search(int *c, int k, int y) +{ + int i, j, l, t; + + if (clist[c[k]].y < y) /* quick look for typical case */ + return (k + 1); + i = 0; + j = k + 1; + for (;;) { + l = (i + j) / 2; + if (l <= i) + break; + t = clist[c[l]].y; + if (t > y) + j = l; + else if (t < y) + i = l; + else + return (l); + } + return (l + 1); +} + +static void +unravel(int p) +{ + struct cand *q; + int i; + + for (i = 0; i <= len[0]; i++) + J[i] = i <= pref ? i : + i > len[0] - suff ? i + len[1] - len[0] : 0; + for (q = clist + p; q->y != 0; q = clist + q->pred) + J[q->x + pref] = q->y + pref; +} + +/* + * Check does double duty: + * 1. ferret out any fortuitous correspondences due + * to confounding by hashing (which result in "jackpot") + * 2. collect random access indexes to the two files + */ +static void +check(FILE *f1, FILE *f2, int flags) +{ + int i, j, jackpot, c, d; + long ctold, ctnew; + + rewind(f1); + rewind(f2); + j = 1; + ixold[0] = ixnew[0] = 0; + jackpot = 0; + ctold = ctnew = 0; + for (i = 1; i <= len[0]; i++) { + if (J[i] == 0) { + ixold[i] = ctold += skipline(f1); + continue; + } + while (j < J[i]) { + ixnew[j] = ctnew += skipline(f2); + j++; + } + if (flags & (D_FOLDBLANKS|D_IGNOREBLANKS|D_IGNORECASE)) { + for (;;) { + c = getc(f1); + d = getc(f2); + /* + * GNU diff ignores a missing newline + * in one file for -b or -w. + */ + if (flags & (D_FOLDBLANKS|D_IGNOREBLANKS)) { + if (c == EOF && d == '\n') { + ctnew++; + break; + } else if (c == '\n' && d == EOF) { + ctold++; + break; + } + } + ctold++; + ctnew++; + if ((flags & D_FOLDBLANKS) && isspace(c) && + isspace(d)) { + do { + if (c == '\n') + break; + ctold++; + } while (isspace(c = getc(f1))); + do { + if (d == '\n') + break; + ctnew++; + } while (isspace(d = getc(f2))); + } else if ((flags & D_IGNOREBLANKS)) { + while (isspace(c) && c != '\n') { + c = getc(f1); + ctold++; + } + while (isspace(d) && d != '\n') { + d = getc(f2); + ctnew++; + } + } + if (chrtran[c] != chrtran[d]) { + jackpot++; + J[i] = 0; + if (c != '\n' && c != EOF) + ctold += skipline(f1); + if (d != '\n' && c != EOF) + ctnew += skipline(f2); + break; + } + if (c == '\n' || c == EOF) + break; + } + } else { + for (;;) { + ctold++; + ctnew++; + if ((c = getc(f1)) != (d = getc(f2))) { + /* jackpot++; */ + J[i] = 0; + if (c != '\n' && c != EOF) + ctold += skipline(f1); + if (d != '\n' && c != EOF) + ctnew += skipline(f2); + break; + } + if (c == '\n' || c == EOF) + break; + } + } + ixold[i] = ctold; + ixnew[j] = ctnew; + j++; + } + for (; j <= len[1]; j++) + ixnew[j] = ctnew += skipline(f2); + /* + * if (jackpot) + * fprintf(stderr, "jackpot\n"); + */ +} + +/* shellsort CACM #201 */ +static void +sort(struct line *a, int n) +{ + struct line *ai, *aim, w; + int j, m = 0, k; + + if (n == 0) + return; + for (j = 1; j <= n; j *= 2) + m = 2 * j - 1; + for (m /= 2; m != 0; m /= 2) { + k = n - m; + for (j = 1; j <= k; j++) { + for (ai = &a[j]; ai > a; ai -= m) { + aim = &ai[m]; + if (aim < ai) + break; /* wraparound */ + if (aim->value > ai[0].value || + (aim->value == ai[0].value && + aim->serial > ai[0].serial)) + break; + w.value = ai[0].value; + ai[0].value = aim->value; + aim->value = w.value; + w.serial = ai[0].serial; + ai[0].serial = aim->serial; + aim->serial = w.serial; + } + } + } +} + +static void +unsort(struct line *f, int l, int *b) +{ + int *a, i; + + a = xcalloc(l + 1, sizeof(*a)); + for (i = 1; i <= l; i++) + a[f[i].serial] = f[i].value; + for (i = 1; i <= l; i++) + b[i] = a[i]; + free(a); +} + +static int +skipline(FILE *f) +{ + int i, c; + + for (i = 1; (c = getc(f)) != '\n' && c != EOF; i++) + continue; + return (i); +} + +static void +output(char *file1, FILE *f1, char *file2, FILE *f2, int flags) +{ + int m, i0, i1, j0, j1; + + rewind(f1); + rewind(f2); + m = len[0]; + J[0] = 0; + J[m + 1] = len[1] + 1; + if (diff_format != D_EDIT) { + for (i0 = 1; i0 <= m; i0 = i1 + 1) { + while (i0 <= m && J[i0] == J[i0 - 1] + 1) + i0++; + j0 = J[i0 - 1] + 1; + i1 = i0 - 1; + while (i1 < m && J[i1 + 1] == 0) + i1++; + j1 = J[i1 + 1] - 1; + J[i1] = j1; + change(file1, f1, file2, f2, i0, i1, j0, j1, &flags); + } + } else { + for (i0 = m; i0 >= 1; i0 = i1 - 1) { + while (i0 >= 1 && J[i0] == J[i0 + 1] - 1 && J[i0] != 0) + i0--; + j0 = J[i0 + 1] - 1; + i1 = i0 + 1; + while (i1 > 1 && J[i1 - 1] == 0) + i1--; + j1 = J[i1 - 1] + 1; + J[i1] = j1; + change(file1, f1, file2, f2, i1, i0, j1, j0, &flags); + } + } + if (m == 0) + change(file1, f1, file2, f2, 1, 0, 1, len[1], &flags); + if (diff_format == D_IFDEF) { + for (;;) { +#define c i0 + if ((c = getc(f1)) == EOF) + return; + diff_output("%c", c); + } +#undef c + } + if (anychange != 0) { + if (diff_format == D_CONTEXT) + dump_context_vec(f1, f2, flags); + else if (diff_format == D_UNIFIED) + dump_unified_vec(f1, f2, flags); + } +} + +static void +range(int a, int b, char *separator) +{ + diff_output("%d", a > b ? b : a); + if (a < b) + diff_output("%s%d", separator, b); +} + +static void +uni_range(int a, int b) +{ + if (a < b) + diff_output("%d,%d", a, b - a + 1); + else if (a == b) + diff_output("%d", b); + else + diff_output("%d,0", b); +} + +static char * +preadline(int fd, size_t rlen, off_t off) +{ + char *line; + ssize_t nr; + off_t pos; + + line = xmalloc(rlen + 1); + pos = lseek(fd, 0, SEEK_CUR); + lseek(fd, off, SEEK_SET); + if ((nr = read(fd, line, rlen)) == -1) + err(2, "preadline"); + lseek(fd, pos, SEEK_SET); + if (nr > 0 && line[nr-1] == '\n') + nr--; + line[nr] = '\0'; + return (line); +} + +static int +ignoreline(char *line) +{ +#if 0 + int ret; + + ret = regexec(&ignore_re, line, 0, NULL, 0); + free(line); + return (ret == 0); /* if it matched, it should be ignored. */ +#endif + return 0; +} + +/* + * Indicate that there is a difference between lines a and b of the from file + * to get to lines c to d of the to file. If a is greater then b then there + * are no lines in the from file involved and this means that there were + * lines appended (beginning at b). If c is greater than d then there are + * lines missing from the to file. + */ +static void +change(char *file1, FILE *f1, char *file2, FILE *f2, int a, int b, int c, int d, + int *pflags) +{ + static size_t max_context = 64; + int i; + +restart: + if (diff_format != D_IFDEF && a > b && c > d) + return; + if (ignore_pats != NULL) { + char *line; + /* + * All lines in the change, insert, or delete must + * match an ignore pattern for the change to be + * ignored. + */ + if (a <= b) { /* Changes and deletes. */ + for (i = a; i <= b; i++) { + line = preadline(fileno(f1), + ixold[i] - ixold[i - 1], ixold[i - 1]); + if (!ignoreline(line)) + goto proceed; + } + } + if (a > b || c <= d) { /* Changes and inserts. */ + for (i = c; i <= d; i++) { + line = preadline(fileno(f2), + ixnew[i] - ixnew[i - 1], ixnew[i - 1]); + if (!ignoreline(line)) + goto proceed; + } + } + return; + } +proceed: + if (*pflags & D_HEADER) { + diff_output("%s %s\n", file1, file2); + *pflags &= ~D_HEADER; + } + if (diff_format == D_CONTEXT || diff_format == D_UNIFIED) { + /* + * Allocate change records as needed. + */ + if (context_vec_ptr == context_vec_end - 1) { + ptrdiff_t offset = context_vec_ptr - context_vec_start; + max_context <<= 1; + context_vec_start = xreallocarray(context_vec_start, + max_context, sizeof(*context_vec_start)); + context_vec_end = context_vec_start + max_context; + context_vec_ptr = context_vec_start + offset; + } + if (anychange == 0) { + /* + * Print the context/unidiff header first time through. + */ + print_header(file1, file2); + anychange = 1; + } else if (a > context_vec_ptr->b + (2 * diff_context) + 1 && + c > context_vec_ptr->d + (2 * diff_context) + 1) { + /* + * If this change is more than 'diff_context' lines from the + * previous change, dump the record and reset it. + */ + if (diff_format == D_CONTEXT) + dump_context_vec(f1, f2, *pflags); + else + dump_unified_vec(f1, f2, *pflags); + } + context_vec_ptr++; + context_vec_ptr->a = a; + context_vec_ptr->b = b; + context_vec_ptr->c = c; + context_vec_ptr->d = d; + return; + } + if (anychange == 0) + anychange = 1; + switch (diff_format) { + case D_BRIEF: + return; + case D_NORMAL: + case D_EDIT: + range(a, b, ","); + diff_output("%c", a > b ? 'a' : c > d ? 'd' : 'c'); + if (diff_format == D_NORMAL) + range(c, d, ","); + diff_output("\n"); + break; + case D_REVERSE: + diff_output("%c", a > b ? 'a' : c > d ? 'd' : 'c'); + range(a, b, " "); + diff_output("\n"); + break; + case D_NREVERSE: + if (a > b) + diff_output("a%d %d\n", b, d - c + 1); + else { + diff_output("d%d %d\n", a, b - a + 1); + if (!(c > d)) + /* add changed lines */ + diff_output("a%d %d\n", b, d - c + 1); + } + break; + } + if (diff_format == D_NORMAL || diff_format == D_IFDEF) { + fetch(ixold, a, b, f1, '<', 1, *pflags); + if (a <= b && c <= d && diff_format == D_NORMAL) + diff_output("---\n"); + } + i = fetch(ixnew, c, d, f2, diff_format == D_NORMAL ? '>' : '\0', 0, *pflags); + if (i != 0 && diff_format == D_EDIT) { + /* + * A non-zero return value for D_EDIT indicates that the + * last line printed was a bare dot (".") that has been + * escaped as ".." to prevent ed(1) from misinterpreting + * it. We have to add a substitute command to change this + * back and restart where we left off. + */ + diff_output(".\n"); + diff_output("%ds/.//\n", a + i - 1); + b = a + i - 1; + a = b + 1; + c += i; + goto restart; + } + if ((diff_format == D_EDIT || diff_format == D_REVERSE) && c <= d) + diff_output(".\n"); + if (inifdef) { + diff_output("#endif /* %s */\n", ifdefname); + inifdef = 0; + } +} + +static int +fetch(long *f, int a, int b, FILE *lb, int ch, int oldfile, int flags) +{ + int i, j, c, lastc, col, nc; + + /* + * When doing #ifdef's, copy down to current line + * if this is the first file, so that stuff makes it to output. + */ + if (diff_format == D_IFDEF && oldfile) { + long curpos = ftell(lb); + /* print through if append (a>b), else to (nb: 0 vs 1 orig) */ + nc = f[a > b ? b : a - 1] - curpos; + for (i = 0; i < nc; i++) + diff_output("%c", getc(lb)); + } + if (a > b) + return (0); + if (diff_format == D_IFDEF) { + if (inifdef) { + diff_output("#else /* %s%s */\n", + oldfile == 1 ? "!" : "", ifdefname); + } else { + if (oldfile) + diff_output("#ifndef %s\n", ifdefname); + else + diff_output("#ifdef %s\n", ifdefname); + } + inifdef = 1 + oldfile; + } + for (i = a; i <= b; i++) { + fseek(lb, f[i - 1], SEEK_SET); + nc = f[i] - f[i - 1]; + if (diff_format != D_IFDEF && ch != '\0') { + diff_output("%c", ch); +#if 0 + if (Tflag && (diff_format == D_NORMAL || diff_format == D_CONTEXT + || diff_format == D_UNIFIED)) + diff_output("\t"); + else +#endif + if (diff_format != D_UNIFIED) + diff_output(" "); + } + col = 0; + for (j = 0, lastc = '\0'; j < nc; j++, lastc = c) { + if ((c = getc(lb)) == EOF) { + if (diff_format == D_EDIT || diff_format == D_REVERSE || + diff_format == D_NREVERSE) + warnx("No newline at end of file"); + else +#if 0 + diff_output("\n\\ No newline at end of " + "file\n"); +#else + diff_output("\n"); +#endif + return (0); + } + if (c == '\t' && (flags & D_EXPANDTABS)) { + do { + diff_output(" "); + } while (++col & 7); + } else { + if (diff_format == D_EDIT && j == 1 && c == '\n' + && lastc == '.') { + /* + * Don't print a bare "." line + * since that will confuse ed(1). + * Print ".." instead and return, + * giving the caller an offset + * from which to restart. + */ + diff_output(".\n"); + return (i - a + 1); + } + diff_output("%c", c); + col++; + } + } + } + return (0); +} + +/* + * Hash function taken from Robert Sedgewick, Algorithms in C, 3d ed., p 578. + */ +static int +readhash(FILE *f, int flags) +{ + int i, t, space; + int sum; + + sum = 1; + space = 0; + if ((flags & (D_FOLDBLANKS|D_IGNOREBLANKS)) == 0) { + if (flags & D_IGNORECASE) + for (i = 0; (t = getc(f)) != '\n'; i++) { + if (t == EOF) { + if (i == 0) + return (0); + break; + } + sum = sum * 127 + chrtran[t]; + } + else + for (i = 0; (t = getc(f)) != '\n'; i++) { + if (t == EOF) { + if (i == 0) + return (0); + break; + } + sum = sum * 127 + t; + } + } else { + for (i = 0;;) { + switch (t = getc(f)) { + case '\t': + case '\r': + case '\v': + case '\f': + case ' ': + space++; + continue; + default: + if (space && (flags & D_IGNOREBLANKS) == 0) { + i++; + space = 0; + } + sum = sum * 127 + chrtran[t]; + i++; + continue; + case EOF: + if (i == 0) + return (0); + /* FALLTHROUGH */ + case '\n': + break; + } + break; + } + } + /* + * There is a remote possibility that we end up with a zero sum. + * Zero is used as an EOF marker, so return 1 instead. + */ + return (sum == 0 ? 1 : sum); +} + +static int +asciifile(FILE *f) +{ + unsigned char buf[BUFSIZ]; + size_t cnt; + + if (f == NULL) + return (1); + + rewind(f); + cnt = fread(buf, 1, sizeof(buf), f); + return (memchr(buf, '\0', cnt) == NULL); +} + +#define begins_with(s, pre) (strncmp(s, pre, sizeof(pre)-1) == 0) + +static char * +match_function(const long *f, int pos, FILE *fp) +{ + unsigned char buf[FUNCTION_CONTEXT_SIZE]; + size_t nc; + int last = lastline; + char *state = NULL; + + lastline = pos; + while (pos > last) { + fseek(fp, f[pos - 1], SEEK_SET); + nc = f[pos] - f[pos - 1]; + if (nc >= sizeof(buf)) + nc = sizeof(buf) - 1; + nc = fread(buf, 1, nc, fp); + if (nc > 0) { + buf[nc] = '\0'; + buf[strcspn((const char *)buf, "\n")] = '\0'; + if (isalpha(buf[0]) || buf[0] == '_' || buf[0] == '$') { + if (begins_with((const char *)buf, "private:")) { + if (!state) + state = " (private)"; + } else if (begins_with((const char *)buf, "protected:")) { + if (!state) + state = " (protected)"; + } else if (begins_with((const char *)buf, "public:")) { + if (!state) + state = " (public)"; + } else { + strncpy(lastbuf, (const char *)buf, sizeof lastbuf); + if (state) + strncat(lastbuf, (const char *)state, sizeof lastbuf); + lastmatchline = pos; + return lastbuf; + } + } + } + pos--; + } + return lastmatchline > 0 ? lastbuf : NULL; +} + +/* dump accumulated "context" diff changes */ +static void +dump_context_vec(FILE *f1, FILE *f2, int flags) +{ + struct context_vec *cvp = context_vec_start; + int lowa, upb, lowc, upd, do_output; + int a, b, c, d; + char ch, *f; + + if (context_vec_start > context_vec_ptr) + return; + + b = d = 0; /* gcc */ + lowa = MAXIMUM(1, cvp->a - diff_context); + upb = MINIMUM(len[0], context_vec_ptr->b + diff_context); + lowc = MAXIMUM(1, cvp->c - diff_context); + upd = MINIMUM(len[1], context_vec_ptr->d + diff_context); + + diff_output("***************"); + if ((flags & D_PROTOTYPE)) { + f = match_function(ixold, lowa-1, f1); + if (f != NULL) + diff_output(" %s", f); + } + diff_output("\n*** "); + range(lowa, upb, ","); + diff_output(" ****\n"); + + /* + * Output changes to the "old" file. The first loop suppresses + * output if there were no changes to the "old" file (we'll see + * the "old" lines as context in the "new" list). + */ + do_output = 0; + for (; cvp <= context_vec_ptr; cvp++) + if (cvp->a <= cvp->b) { + cvp = context_vec_start; + do_output++; + break; + } + if (do_output) { + while (cvp <= context_vec_ptr) { + a = cvp->a; + b = cvp->b; + c = cvp->c; + d = cvp->d; + + if (a <= b && c <= d) + ch = 'c'; + else + ch = (a <= b) ? 'd' : 'a'; + + if (ch == 'a') + fetch(ixold, lowa, b, f1, ' ', 0, flags); + else { + fetch(ixold, lowa, a - 1, f1, ' ', 0, flags); + fetch(ixold, a, b, f1, + ch == 'c' ? '!' : '-', 0, flags); + } + lowa = b + 1; + cvp++; + } + fetch(ixold, b + 1, upb, f1, ' ', 0, flags); + } + /* output changes to the "new" file */ + diff_output("--- "); + range(lowc, upd, ","); + diff_output(" ----\n"); + + do_output = 0; + for (cvp = context_vec_start; cvp <= context_vec_ptr; cvp++) + if (cvp->c <= cvp->d) { + cvp = context_vec_start; + do_output++; + break; + } + if (do_output) { + while (cvp <= context_vec_ptr) { + a = cvp->a; + b = cvp->b; + c = cvp->c; + d = cvp->d; + + if (a <= b && c <= d) + ch = 'c'; + else + ch = (a <= b) ? 'd' : 'a'; + + if (ch == 'd') + fetch(ixnew, lowc, d, f2, ' ', 0, flags); + else { + fetch(ixnew, lowc, c - 1, f2, ' ', 0, flags); + fetch(ixnew, c, d, f2, + ch == 'c' ? '!' : '+', 0, flags); + } + lowc = d + 1; + cvp++; + } + fetch(ixnew, d + 1, upd, f2, ' ', 0, flags); + } + context_vec_ptr = context_vec_start - 1; +} + +/* dump accumulated "unified" diff changes */ +static void +dump_unified_vec(FILE *f1, FILE *f2, int flags) +{ + struct context_vec *cvp = context_vec_start; + int lowa, upb, lowc, upd; + int a, b, c, d; + char ch, *f; + + if (context_vec_start > context_vec_ptr) + return; + + b = d = 0; /* gcc */ + lowa = MAXIMUM(1, cvp->a - diff_context); + upb = MINIMUM(len[0], context_vec_ptr->b + diff_context); + lowc = MAXIMUM(1, cvp->c - diff_context); + upd = MINIMUM(len[1], context_vec_ptr->d + diff_context); + + diff_output("@@ -"); + uni_range(lowa, upb); + diff_output(" +"); + uni_range(lowc, upd); + diff_output(" @@"); + if ((flags & D_PROTOTYPE)) { + f = match_function(ixold, lowa-1, f1); + if (f != NULL) + diff_output(" %s", f); + } + diff_output("\n"); + + /* + * Output changes in "unified" diff format--the old and new lines + * are printed together. + */ + for (; cvp <= context_vec_ptr; cvp++) { + a = cvp->a; + b = cvp->b; + c = cvp->c; + d = cvp->d; + + /* + * c: both new and old changes + * d: only changes in the old file + * a: only changes in the new file + */ + if (a <= b && c <= d) + ch = 'c'; + else + ch = (a <= b) ? 'd' : 'a'; + + switch (ch) { + case 'c': + fetch(ixold, lowa, a - 1, f1, ' ', 0, flags); + fetch(ixold, a, b, f1, '-', 0, flags); + fetch(ixnew, c, d, f2, '+', 0, flags); + break; + case 'd': + fetch(ixold, lowa, a - 1, f1, ' ', 0, flags); + fetch(ixold, a, b, f1, '-', 0, flags); + break; + case 'a': + fetch(ixnew, lowc, c - 1, f2, ' ', 0, flags); + fetch(ixnew, c, d, f2, '+', 0, flags); + break; + } + lowa = b + 1; + lowc = d + 1; + } + fetch(ixnew, d + 1, upd, f2, ' ', 0, flags); + + context_vec_ptr = context_vec_start - 1; +} + +static void +print_header(const char *file1, const char *file2) +{ + if (label[0] != NULL) + diff_output("%s %s\n", diff_format == D_CONTEXT ? "***" : "---", + label[0]); + else + diff_output("%s %s\t%s", diff_format == D_CONTEXT ? "***" : "---", + file1, ctime(&stb1.st_mtime)); + if (label[1] != NULL) + diff_output("%s %s\n", diff_format == D_CONTEXT ? "---" : "+++", + label[1]); + else + diff_output("%s %s\t%s", diff_format == D_CONTEXT ? "---" : "+++", + file2, ctime(&stb2.st_mtime)); +} --- main.c Mon Oct 18 09:22:19 2021 +++ main.c Mon Oct 18 09:22:19 2021 @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2020-2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdio.h> +#include <string.h> + +#include "amend.h" +#include "browser.h" +#include "committer.h" +#include "repo.h" +#include "util.h" + +MenuHandle file_menu, edit_menu, repo_menu; +Handle commit_list_ldef_h; +short quitting = 0; +struct browser *cur_browser = NULL; + +void handle_menu(long menu_id); +void cur_browser_close(void); +void update_menu(void); + +int +main(void) +{ + Handle mbar; + MenuHandle apple_menu; + EventRecord event; + WindowPtr event_win; + GrafPtr old_port; + struct repo *repo; + short event_in, i, did_initial_open = 0; + char key; + + InitGraf(&thePort); + InitFonts(); + FlushEvents(everyEvent, 0); + InitWindows(); + InitMenus(); + TEInit(); + InitDialogs(0); + InitCursor(); + MaxApplZone(); + + err_init(); + + mbar = GetNewMBar(MBAR_ID); + SetMenuBar(mbar); + apple_menu = GetMHandle(APPLE_MENU_ID); + AddResMenu(apple_menu, 'DRVR'); + file_menu = GetMHandle(FILE_MENU_ID); + edit_menu = GetMHandle(EDIT_MENU_ID); + repo_menu = GetMHandle(REPO_MENU_ID); + update_menu(); + + /* dynamically patch list LDEF to point to our *_list_ldef funcs */ + commit_list_ldef_h = GetResource('LDEF', COMMIT_LDEF_ID); + if (!commit_list_ldef_h) + err(1, "Can't find commit list LDEF %d", COMMIT_LDEF_ID); + HLock(commit_list_ldef_h); + ((tCodeStub *)*commit_list_ldef_h)->addr = &commit_list_ldef; + + while (!quitting) { + WaitNextEvent(everyEvent, &event, 5L, 0L); + + switch (event.what) { + case nullEvent: + if (cur_browser) + browser_idle(cur_browser); + else if (!did_initial_open) { + repo = repo_open(); + if (repo) + cur_browser = browser_init(repo); + did_initial_open = 1; + } + break; + case keyDown: + case autoKey: + key = event.message & charCodeMask; + if ((event.modifiers & cmdKey) != 0) + handle_menu(MenuKey(key)); + else if (cur_browser && cur_browser->committer && + FrontWindow() == cur_browser->committer->win) + committer_key_down(cur_browser->committer, &event); + break; + case mouseDown: + event_in = FindWindow(event.where, &event_win); + + switch (event_in) { + case inMenuBar: + handle_menu(MenuSelect(event.where)); + break; + case inSysWindow: + SystemClick(&event, event_win); + break; + case inDrag: + DragWindow(event_win, event.where, &screenBits.bounds); + break; + case inGoAway: + if (TrackGoAway(event_win, event.where)) { + if (cur_browser && event_win == cur_browser->win) + cur_browser_close(); + else if (cur_browser && cur_browser->committer && + event_win == cur_browser->committer->win) + browser_close_committer(cur_browser); + } + break; + case inContent: + if (event_win != FrontWindow()) + SelectWindow(event_win); + if (event_win == cur_browser->win) + browser_mouse_down(cur_browser, &event); + else if (cur_browser && cur_browser->committer && + event_win == cur_browser->committer->win) + committer_mouse_down(cur_browser->committer, &event); + break; + } + break; + case updateEvt: + case activateEvt: + event_win = (WindowPtr)event.message; + + if (event.what == updateEvt) { + GetPort(&old_port); + SetPort(event_win); + BeginUpdate(event_win); + } + + if (cur_browser) { + if (event_win == cur_browser->win) + browser_update(cur_browser, &event); + else if (cur_browser->committer && + event_win == cur_browser->committer->win) + committer_update(cur_browser->committer, &event); + } + + if (event.what == updateEvt) { + EndUpdate(event_win); + SetPort(old_port); + } + break; + } + } + + return 0; +} + +void +cur_browser_close(void) +{ + browser_close(cur_browser); + cur_browser = NULL; + update_menu(); +} + +void +handle_menu(long menu_id) +{ + switch (HiWord(menu_id)) { + case APPLE_MENU_ID: + switch (LoWord(menu_id)) { + case APPLE_MENU_ABOUT_ID: { + VersRecHndl vers; + char vers_s[255]; + char short_vers[255] = { 0 }; + + if ((vers = (VersRecHndl)GetResource('vers', 1))) { + HLock(vers); + memcpy(short_vers, (*vers)->shortVersion + 3, + ((*vers)->shortVersion)[2]); + sprintf(vers_s, "%s %s", PROGRAM_NAME, short_vers); + ReleaseResource(vers); + note("%s", vers_s); + } else + warnx("Can't find version number!"); + break; + } + } + break; + case FILE_MENU_ID: + switch (LoWord(menu_id)) { + case FILE_MENU_NEW_ID: { + struct repo *repo; + if (cur_browser) + cur_browser_close(); + if ((repo = repo_create())) + cur_browser = browser_init(repo); + break; + } + case FILE_MENU_OPEN_ID: { + struct repo *repo; + /* TODO: don't close unless we open a new repo */ + if (cur_browser) + cur_browser_close(); + if ((repo = repo_open())) + cur_browser = browser_init(repo); + break; + } + case FILE_MENU_QUIT_ID: + if (cur_browser) { + browser_close(cur_browser); + cur_browser = NULL; + } + quitting = 1; + break; + } + break; + case EDIT_MENU_ID: + /* TODO: detect whether to operate on committer diff or log */ + switch (LoWord(menu_id)) { + case EDIT_MENU_COPY_ID: + if (cur_browser && cur_browser->committer) + TECopyRemovingFakeTabs(cur_browser->committer->diff_te); + else if (cur_browser) + TECopyRemovingFakeTabs(cur_browser->diff_te); + break; + case EDIT_MENU_SELECT_ALL_ID: + if (cur_browser && cur_browser->committer) + TESetSelect(0, 1024 * 32, cur_browser->committer->diff_te); + else if (cur_browser) + TESetSelect(0, 1024 * 32, cur_browser->diff_te); + break; + } + break; + case REPO_MENU_ID: + switch (LoWord(menu_id)) { + case REPO_MENU_ADD_FILE_ID: + if (cur_browser) + cur_browser->state = BROWSER_STATE_ADD_FILE; + break; + } + break; + } + + HiliteMenu(0); +} + +void +update_menu(void) +{ + if (cur_browser) { + browser_update_menu(cur_browser); + return; + } + + DisableItem(edit_menu, EDIT_MENU_SELECT_ALL_ID); + DisableItem(edit_menu, EDIT_MENU_COPY_ID); + DisableItem(repo_menu, 0); + DrawMenuBar(); +} --- repo.c Mon Oct 18 17:12:17 2021 +++ repo.c Tue Oct 19 12:56:48 2021 @@ -0,0 +1,746 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdio.h> +#include <string.h> +#include <time.h> + +#include "amend.h" +#include "diff.h" +#include "repo.h" +#include "util.h" + +struct repo *repo_init(SFReply reply); +void repo_sort_files(struct repo *repo); +void repo_sort_commits(struct repo *repo); +short repo_get_file_attrs(struct repo *repo, Str255 filename, + struct repo_file_attrs *attrs); +short repo_file_update(struct repo *repo, struct repo_file *file); + +struct repo * +repo_open(void) +{ + Point pt = { 75, 100 }; + SFReply reply; + SFTypeList types; + + types[0] = REPO_TYPE; + + SFGetFile(pt, NULL, NULL, 1, &types, NULL, &reply); + if (!reply.good) + return NULL; + + return repo_init(reply); +} + +struct repo * +repo_create(void) +{ + Point pt = { 75, 100 }; + SFReply reply; + struct tmpl { + Handle h; + ResType type; + Str255 name; + short id; + } tmpls[10] = { NULL }; + char *newpath = NULL; + short error, fh, i, ntmpls; + + SFPutFile(pt, "\pCreate new repository:", NULL, NULL, &reply); + if (!reply.good) + return NULL; + + getpath(reply.vRefNum, reply.fName, &newpath, 1); + CtoPstr(newpath); + + /* copy TMPL from our own resource file */ + ntmpls = Count1Resources('TMPL'); + if (ntmpls > nitems(tmpls)) + err(1, "ntmpls %d > %d", ntmpls, nitems(tmpls)); + for (i = 0; i < ntmpls; i++) { + tmpls[i].h = GetIndResource('TMPL', i + 1); + if (tmpls[i].h == NULL) + err(1, "Failed fetching TMPL %d", i + 1); + + GetResInfo(tmpls[i].h, &tmpls[i].id, &tmpls[i].type, + &tmpls[i].name); + DetachResource(tmpls[i].h); + } + + error = Create(reply.fName, reply.vRefNum, REPO_CREATOR, REPO_TYPE); + if (error != 0) + err(1, "Failed to create %s: %d", PtoCstr(reply.fName), error); + + CreateResFile(newpath); + if (ResError() != 0) + err(1, "Failed to open %s: %d", PtoCstr(reply.fName), ResError()); + + fh = OpenResFile(newpath); + if (fh == -1) + err(1, "Failed to open %s: %d", PtoCstr(reply.fName), ResError()); + + for (i = 0; i < ntmpls; i++) + AddResource(tmpls[i].h, tmpls[i].type, tmpls[i].id, tmpls[i].name); + + CloseResFile(fh); + free(newpath); + + return repo_init(reply); +} + +struct repo * +repo_init(SFReply reply) +{ + Str255 buf; + struct repo *repo; + char *newpath; + Handle resh; + short error, fh, i; + + getpath(reply.vRefNum, reply.fName, &newpath, 1); + CtoPstr(newpath); + + fh = OpenResFile(newpath); + if (fh == -1) + err(1, "failed to open %s: %d", PtoCstr(newpath), ResError()); + + repo = xmalloczero(sizeof(struct repo)); + repo->fh = fh; + repo->vrefnum = reply.vRefNum; + repo->tabwidth = 4; // (short)safe_GetStringAsLong(TABWIDTH_ID); + repo->next_file_id = 1; + repo->next_commit_id = 1; + + memcpy(repo->filename, reply.fName, reply.fName[0] + 1); + PtoCstr(repo->filename); + + /* fill in file info */ + repo->nfiles = Count1Resources(REPO_FILE_RTYPE); + if (repo->nfiles) { + repo->files = xmalloc(repo->nfiles * sizeof(Ptr)); + for (i = 0; i < repo->nfiles; i++) { + resh = GetIndResource(REPO_FILE_RTYPE, i + 1); + if (resh == NULL) + err(1, "failed fetching file %d", i + 1); + HLock(resh); + repo->files[i] = repo_parse_file(resh); + if (repo->files[i]->id >= repo->next_file_id) + repo->next_file_id = repo->files[i]->id + 1; + ReleaseResource(resh); + } + } + repo_sort_files(repo); + + /* fill in commit info */ + repo->ncommits = Count1Resources(REPO_COMMIT_RTYPE); + if (repo->ncommits) { + repo->commits = xmalloc(repo->ncommits * sizeof(Ptr)); + for (i = 0; i < repo->ncommits; i++) { + resh = GetIndResource(REPO_COMMIT_RTYPE, i + 1); + if (resh == NULL) + err(1, "failed fetching commit %d", i + 1); + repo->commits[i] = repo_parse_commit(resh); + if (repo->commits[i]->id >= repo->next_commit_id) + repo->next_commit_id = repo->commits[i]->id + 1; + ReleaseResource(resh); + } + } + repo_sort_commits(repo); + + return repo; +} + +void +repo_close(struct repo *repo) +{ + struct repo_file *file; + struct repo_commit *commit; + short i; + + for (i = 0; i < repo->ncommits; i++) { + commit = repo->commits[i]; + if (commit == NULL) + continue; + + if (commit->log != NULL) + DisposHandle(commit->log); + + if (commit->file_ids != NULL) + free(commit->file_ids); + + free(commit); + } + free(repo->commits); + + for (i = 0; i < repo->nfiles; i++) { + file = repo->files[i]; + if (file == NULL) + continue; + + free(file); + } + free(repo->files); + + CloseResFile(repo->vrefnum); + free(repo); +} + +struct repo_file * +repo_parse_file(Handle hfile) +{ + struct repo_file *file; + short len, i; + unsigned char *data; + Str255 buf; + + HLock(hfile); + data = (unsigned char *)(*hfile); + + file = xmalloczero(sizeof(struct repo_file)); + + GetResInfo(hfile, &file->id, &buf, &buf); + + /* filename, pstr */ + len = data[0]; + memcpy(file->filename, data + 1, len); + file->filename[len] = '\0'; + data += (data[0] + 1); + + /* type fourcc, creator fourcc */ + memcpy(file->type, data, 4); + data += 4; + memcpy(file->creator, data, 4); + data += 4; + + /* creation date, long */ + file->ctime = ((unsigned long)data[0] << 24) | + ((unsigned long)data[1] << 16) | + ((unsigned long)data[2] << 8) | + ((unsigned long)data[3]); + data += 4; + + /* modification date, long */ + file->mtime = ((unsigned long)data[0] << 24) | + ((unsigned long)data[1] << 16) | + ((unsigned long)data[2] << 8) | + ((unsigned long)data[3]); + data += 4; + + HUnlock(hfile); + + return file; +} + +struct repo_commit * +repo_parse_commit(Handle hcommit) +{ + struct repo_commit *commit; + short len, i; + unsigned char *data; + Str255 buf; + + HLock(hcommit); + data = (unsigned char *)(*hcommit); + + commit = xmalloczero(sizeof(struct repo_commit)); + + GetResInfo(hcommit, &commit->id, &buf, &buf); + + /* date */ + commit->date = ((unsigned long)data[0] << 24) | + ((unsigned long)data[1] << 16) | + ((unsigned long)data[2] << 8) | + ((unsigned long)data[3]); + data += 4; + + /* author, pstr */ + len = data[0]; + if (len > sizeof(commit->author) - 1) + len = sizeof(commit->author) - 1; + memcpy(commit->author, data + 1, len); + commit->author[len] = '\0'; + data += (data[0] + 1); + + /* files, short */ + commit->nfiles = (data[0] << 8) | data[1]; + data += 2; + + if (commit->nfiles) { + commit->file_ids = xmalloc(sizeof(short) * commit->nfiles); + for (i = 0; i < commit->nfiles; i++) { + commit->file_ids[i] = (data[0] << 8) | data[1]; + data += 2; + } + } + + /* additions, short */ + commit->adds = (data[0] << 8) | data[1]; + data += 2; + + /* subs, short */ + commit->subs = (data[0] << 8) | data[1]; + data += 2; + + /* log message, word-length */ + len = (data[0] << 8) | data[1]; + data += 2; + commit->log = xNewHandle(len + 1); + commit->log_len = len; + HLock(commit->log); + memcpy(*(commit->log), data, len); + (*(commit->log))[len] = '\0'; + HUnlock(commit->log); + data += len; + + HUnlock(hcommit); + + return commit; +} + +struct repo_file * +repo_file_with_id(struct repo *repo, short id) +{ + short i; + + for (i = 0; i < repo->nfiles; i++) + if (repo->files[i]->id == id) + return repo->files[i]; + + return NULL; +} + +void +repo_show_diff_text(struct repo_commit *commit, TEHandle te) +{ + Handle diffh; + + diffh = Get1Resource(REPO_DIFF_RTYPE, commit->id); + if (diffh == NULL) + err(1, "failed finding DIFF %d", commit->id); + + HLock(diffh); + TESetText(*diffh, GetHandleSize(diffh), te); + ReleaseResource(diffh); +} + +struct repo_file * +repo_add_file(struct repo *repo) +{ + Point pt = { 75, 100 }; + SFReply reply; + Str255 repofname; + Handle new_fileh; + struct repo_file *file; + struct repo_file_attrs attrs; + unsigned long zero; + char *repopath = NULL, *newpath = NULL, *data; + short i; + + SFGetFile(pt, "\p", NULL, -1, 0, NULL, &reply); + if (!reply.good) + return NULL; + + memcpy(repofname, repo->filename, strlen(repo->filename) + 1); + CtoPstr(&repofname); + + /* if the file is not in the same dir as the repo, bail */ + getpath(repo->vrefnum, repofname, &repopath, false); + if (repopath == NULL) + err(1, "Can't find path to repo"); + getpath(reply.vRefNum, reply.fName, &newpath, false); + if (newpath == NULL) + err(1, "Can't find path to new file"); + + if (strcmp(repopath, newpath) != 0) { + warn("Can't add files from a directory other than the repo's"); + free(newpath); + free(repopath); + return NULL; + } + + free(repopath); + free(newpath); + + /* make sure the file isn't already in the repo */ + PtoCstr(reply.fName); + for (i = 0; i < repo->nfiles; i++) { + if (strcmp(repo->files[i]->filename, (char *)reply.fName) == 0) { + warn("%s already exists in this repo", reply.fName); + return NULL; + } + } + CtoPstr(reply.fName); + + repo->nfiles++; + repo->files = xrealloc(repo->files, repo->nfiles * sizeof(Ptr)); + file = repo->files[repo->nfiles - 1] = + xmalloc(sizeof(struct repo_file)); + + file->id = repo->next_file_id; + repo->next_file_id++; + memcpy(&file->filename, reply.fName + 1, reply.fName[0]); + file->filename[reply.fName[0]] = '\0'; + + /* fetch type/creator/dates, store resource */ + repo_file_update(repo, file); + + repo_sort_files(repo); + + return file; +} + +short +repo_file_update(struct repo *repo, struct repo_file *file) +{ + struct repo_file_attrs attrs; + Str255 filename = { 0 }; + Handle fileh; + short error, len, new = 0; + char *data; + + memcpy(filename, file->filename, strlen(file->filename)); + CtoPstr(filename); + + error = repo_get_file_attrs(repo, filename, &attrs); + if (error) { + warn("Failed to get info for %s", file->filename); + return -1; + } + memcpy(&file->type, &attrs.type, 4); + memcpy(&file->creator, &attrs.creator, 4); + memcpy(&file->ctime, &attrs.ctime, 4); + memcpy(&file->mtime, &attrs.mtime, 4); + + /* filename len, filename, type, creator, ctime, mtime */ + len = 1 + filename[0] + 4 + 4 + 4 + 4; + + fileh = Get1Resource(REPO_FILE_RTYPE, file->id); + if (fileh == NULL) { + /* build a new AFIL resource */ + fileh = xNewHandle(len); + new = 1; + } + + HLock(fileh); + + data = (char *)(*fileh); + memset(data, 0, len); + + /* copy filename as pstr */ + memcpy(data, filename, filename[0] + 1); + data += filename[0] + 1; + + /* file type, creator, and dates */ + memcpy(data, &attrs.type, 4); + data += 4; + memcpy(data, &attrs.creator, 4); + data += 4; + memcpy(data, &attrs.ctime, 4); + data += 4; + memcpy(data, &attrs.mtime, 4); + data += 4; + + if (new) + AddResource(fileh, REPO_FILE_RTYPE, file->id, filename); + + WriteResource(fileh); + ReleaseResource(fileh); +} + +short +repo_get_file_attrs(struct repo *repo, Str255 filename, + struct repo_file_attrs *attrs) +{ + FInfo fi; + short error; + struct stat sb; + char *filepath = NULL; + + /* lookup file type and creator */ + if (GetFInfo(filename, repo->vrefnum, &fi) != 0) { + warn("Failed to GetFInfo %s", PtoCstr(filename)); + return -1; + } + + memcpy(&attrs->type, &fi.fdType, 4); + memcpy(&attrs->creator, &fi.fdCreator, 4); + + getpath(repo->vrefnum, filename, &filepath, true); + error = stat(filepath, &sb); + free(filepath); + if (error) { + warn("Failed to stat %s", filepath); + return -1; + } + + attrs->ctime = sb.st_ctime; + attrs->mtime = sb.st_mtime; + + return 0; +} + +void +repo_sort_files(struct repo *repo) +{ + struct repo_file *file; + short i, j; + + for (i = 0; i < repo->nfiles; i++) { + for (j = 0; j < repo->nfiles - i - 1; j++) { + if (strnatcasecmp(repo->files[j]->filename, + repo->files[j + 1]->filename) == 1) { + file = repo->files[j]; + repo->files[j] = repo->files[j + 1]; + repo->files[j + 1] = file; + } + } + } +} + +void +repo_sort_commits(struct repo *repo) +{ + struct repo_commit *commit; + short i, j; + + /* reverse order, newest commit first */ + for (i = 0; i < repo->ncommits; i++) { + for (j = 0; j < repo->ncommits - i - 1; j++) { + if (repo->commits[j]->id < repo->commits[j + 1]->id) { + commit = repo->commits[j]; + repo->commits[j] = repo->commits[j + 1]; + repo->commits[j + 1] = commit; + } + } + } +} + +short +repo_diff_file(struct repo *repo, struct repo_file *file) +{ + Handle texth; + Str255 fromfilename = { 0 }, tofilename = { 0 }; + struct repo_file_attrs attrs; + char *fromfilepath, *tofilepath; + char label0[64], label1[64]; + long len; + short error, ret, frefnum; + + /* write out old file */ + sprintf((char *)fromfilename, "%s (%s tmp)", file->filename, + PROGRAM_NAME); + CtoPstr(fromfilename); + + error = Create(fromfilename, repo->vrefnum, (OSType)file->creator, + (OSType)file->type); + if (error && error != dupFNErr) + err(1, "Failed to create file %s: %d", PtoCstr(fromfilename), + error); + + error = FSOpen(fromfilename, repo->vrefnum, &frefnum); + if (error) + err(1, "Failed to open file %s: %d", PtoCstr(fromfilename), error); + + error = SetEOF(frefnum, 0); + if (error) + err(1, "Failed to truncate file %s: %d", PtoCstr(fromfilename), + error); + + getpath(repo->vrefnum, fromfilename, &fromfilepath, true); + + texth = Get1Resource(REPO_TEXT_RTYPE, file->id); + /* if there's no existing TEXT resource, it's a new file */ + if (texth != NULL) { + len = GetHandleSize(texth); + HLock(texth); + error = FSWrite(frefnum, &len, *texth); + if (error) + err(1, "Failed to write old file to %s: %d", + PtoCstr(fromfilename), error); + ReleaseResource(texth); + } + + FSClose(frefnum); + + memcpy((char *)tofilename, file->filename, sizeof(file->filename)); + CtoPstr(tofilename); + getpath(repo->vrefnum, tofilename, &tofilepath, true); + + error = repo_get_file_attrs(repo, tofilename, &attrs); + if (error) + err(1, "Failed to get info for %s", PtoCstr(tofilename)); + + /* specify diff header labels to avoid printing tmp filename */ + /* (TODO: use paths relative to repo) */ + label[0] = (char *)&label0; + sprintf(label0, "%s\t%s", file->filename, + ctime(file->mtime ? &file->mtime : &attrs.mtime)); + label[1] = (char *)&label1; + sprintf(label1, "%s\t%s", file->filename, ctime(&attrs.mtime)); + + /* ctime is stupid, remove trailing \n */ + label0[strlen(label0) - 1] = '\0'; + label1[strlen(label1) - 1] = '\0'; + + ret = diffreg(fromfilepath, tofilepath, D_IGNOREBLANKS | D_PROTOTYPE); + + /* delete temp file */ + error = FSDelete(fromfilename, repo->vrefnum); + if (error) + err(1, "Failed to delete temp file %s: %d", PtoCstr(fromfilename), + error); + + free(fromfilepath); + free(tofilepath); + + if (ret == D_SAME) + return 0; + + return 1; +} + +void +repo_commit(struct repo *repo, short *files, short nfiles, short adds, + short subs, Handle log, short loglen, Handle diff, unsigned long difflen) +{ + Str255 tfilename; + struct repo_file *file; + Handle commith, texth, fileh; + short commit_len = 0, pos = 0, len; + time_t date; + char author[] = "jcs"; /* XXX */ + long fsize; + short i, commit_id, id, error, frefnum; + + /* date (long) */ + commit_len += sizeof(long); + + /* author (pstr) */ + commit_len += 1 + strlen(author); + + /* nfiles (short) */ + commit_len += sizeof(short) + (nfiles * sizeof(short)); + + /* adds (short) */ + commit_len += sizeof(short); + + /* deletions (short) */ + commit_len += sizeof(short); + + /* log (wstr) */ + commit_len += sizeof(short) + loglen; + + commith = xNewHandle(commit_len); + HLock(commith); + + date = Time; + (*commith)[pos++] = (date >> 24) & 0xff; + (*commith)[pos++] = (date >> 16) & 0xff; + (*commith)[pos++] = (date >> 8) & 0xff; + (*commith)[pos++] = date & 0xff; + + (*commith)[pos++] = strlen(author); + for (i = 0; i < strlen(author); i++) + (*commith)[pos++] = author[i]; + + (*commith)[pos++] = (nfiles >> 8) & 0xff; + (*commith)[pos++] = nfiles & 0xff; + for (i = 0; i < nfiles; i++) { + (*commith)[pos++] = (files[i] >> 8) & 0xff; + (*commith)[pos++] = files[i] & 0xff; + } + + (*commith)[pos++] = (adds >> 8) & 0xff; + (*commith)[pos++] = adds & 0xff; + + (*commith)[pos++] = (subs >> 8) & 0xff; + (*commith)[pos++] = subs & 0xff; + + HLock(log); + (*commith)[pos++] = (loglen >> 8) & 0xff; + (*commith)[pos++] = loglen & 0xff; + memcpy(*commith + pos, *log, loglen); + pos += loglen; + HUnlock(log); + + if (pos != commit_len) + err(1, "Accumulated len %d != expected %d", pos, commit_len); + + commit_id = repo->next_commit_id; + + /* store diff */ + AddResource(diff, REPO_DIFF_RTYPE, commit_id, NULL); + WriteResource(diff); + + /* store commit */ + AddResource(commith, REPO_COMMIT_RTYPE, commit_id, NULL); + WriteResource(commith); + + /* store new versions of each file */ + for (i = 0; i < nfiles; i++) { + file = repo_file_with_id(repo, files[i]); + if (file == NULL) + err(1, "Bogus file %d in commit", files[i]); + + texth = Get1Resource(REPO_TEXT_RTYPE, file->id); + if (texth != NULL) { + RmveResource(texth); + ReleaseResource(texth); + } + + len = strlen(file->filename); + memcpy(tfilename, file->filename, len); + tfilename[len] = '\0'; + CtoPstr(tfilename); + + error = FSOpen(tfilename, repo->vrefnum, &frefnum); + if (error) + err(1, "Failed to open file %s: %d", PtoCstr(tfilename), + error); + + error = GetEOF(frefnum, &fsize); + if (error) + err(1, "Failed to get size of file %s: %d", PtoCstr(tfilename), + error); + + texth = xNewHandle(fsize); + HLock(texth); + error = FSRead(frefnum, &fsize, *texth); + if (error) + err(1, "Failed to read %ul of file %s: %d", fsize, + PtoCstr(tfilename), error); + + FSClose(frefnum); + + AddResource(texth, REPO_TEXT_RTYPE, files[i], tfilename); + WriteResource(texth); + ReleaseResource(texth); + + repo_file_update(repo, file); + } + + repo->next_commit_id = commit_id + 1; + + /* update commit list */ + repo->ncommits++; + repo->commits = xrealloc(repo->commits, repo->ncommits * sizeof(Ptr)); + repo->commits[repo->ncommits - 1] = repo_parse_commit(commith); + + ReleaseResource(commith); + + repo_sort_commits(repo); +} --- repo.h Mon Oct 18 15:08:25 2021 +++ repo.h Tue Oct 19 12:53:10 2021 @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef __REPO_H__ +#define __REPO_H__ + +#include <time.h> + +#define REPO_CREATOR 'AMND' +#define REPO_TYPE 'AMRP' + +#define REPO_FILE_RTYPE 'AFIL' +#define REPO_COMMIT_RTYPE 'CMMT' +#define REPO_DIFF_RTYPE 'DIFF' +#define REPO_TEXT_RTYPE 'TEXT' + +struct repo_file { + short id; + char filename[256]; + char type[4]; + char creator[4]; + unsigned long ctime; + unsigned long mtime; +}; + +struct repo_file_attrs { + char type[4]; + char creator[4]; + unsigned long ctime; + unsigned long mtime; +}; + +struct repo_commit { + short id; + time_t date; + char author[32]; + short nfiles; + short *file_ids; + short adds; + short subs; + short log_len; + Handle log; +}; + +struct repo { + char filename[256]; + short fh; + short vrefnum; + short tabwidth; + short nfiles; + struct repo_file **files; + short next_file_id; + short ncommits; + struct repo_commit **commits; + short next_commit_id; +}; + +struct repo *repo_open(void); +struct repo *repo_create(void); +void repo_close(struct repo *repo); +struct repo_commit *repo_parse_commit(Handle hcommit); +struct repo_file *repo_parse_file(Handle hfile); +struct repo_file *repo_file_with_id(struct repo *repo, short id); +void repo_show_diff_text(struct repo_commit *commit, TEHandle te); +struct repo_file *repo_add_file(struct repo *repo); +short repo_diff_file(struct repo *repo, struct repo_file *file); +void repo_commit(struct repo *repo, short *files, short nfiles, short adds, + short subs, Handle log, short loglen, Handle diff, unsigned long difflen); + +#endif --- strnatcmp.c Wed Oct 13 17:21:09 2021 +++ strnatcmp.c Wed Oct 13 17:21:09 2021 @@ -0,0 +1,157 @@ +/* -*- mode: c; c-file-style: "k&r" -*- + + strnatcmp.c -- Perform 'natural order' comparisons of strings in C. + Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net> + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + + +/* partial change history: + * + * 2004-10-10 mbp: Lift out character type dependencies into macros. + * + * Eric Sosman pointed out that ctype functions take a parameter whose + * value must be that of an unsigned int, even on platforms that have + * negative chars in their default char type. + * + * 2021-10-13 jcs: Modified to compile in THINK C 5 + */ + +#include <stddef.h> /* size_t */ +#include <ctype.h> + +#include "util.h" + +int compare_right(char const *a, char const *b); + +int +compare_right(char const *a, char const *b) +{ + int bias = 0; + + /* The longest run of digits wins. That aside, the greatest + value wins, but we can't know that it will until we've scanned + both numbers to know that they have the same magnitude, so we + remember it in BIAS. */ + for (;; a++, b++) { + if (!isdigit((unsigned char)*a) && !isdigit((unsigned char)*b)) + return bias; + if (!isdigit((unsigned char)*a)) + return -1; + if (!isdigit((unsigned char)*b)) + return +1; + if (*a < *b) { + if (!bias) + bias = -1; + } else if (*a > *b) { + if (!bias) + bias = +1; + } else if (!*a && !*b) + return bias; + } + + return 0; +} + + +static int +compare_left(char const *a, char const *b) +{ + /* Compare two left-aligned numbers: the first to have a + different value wins. */ + for (;; a++, b++) { + if (!isdigit((unsigned char)*a) && !isdigit((unsigned char)*b)) + return 0; + if (!isdigit((unsigned char)*a)) + return -1; + if (!isdigit((unsigned char)*b)) + return +1; + if (*a < *b) + return -1; + if (*a > *b) + return +1; + } + + return 0; +} + + +static int +strnatcmp0(char const *a, char const *b, int fold_case) +{ + int ai, bi; + char ca, cb; + int fractional, result; + + ai = bi = 0; + while (1) { + ca = a[ai]; cb = b[bi]; + + /* skip over leading spaces or zeros */ + while (isspace((unsigned char)ca)) + ca = a[++ai]; + + while (isspace((unsigned char)cb)) + cb = b[++bi]; + + /* process run of digits */ + if (isdigit((unsigned char)ca) && isdigit((unsigned char)cb)) { + fractional = (ca == '0' || cb == '0'); + + if (fractional) { + if ((result = compare_left(a+ai, b+bi)) != 0) + return result; + } else { + if ((result = compare_right(a+ai, b+bi)) != 0) + return result; + } + } + + if (!ca && !cb) { + /* The strings compare the same. Perhaps the caller + will want to call strcmp to break the tie. */ + return 0; + } + + if (fold_case) { + ca = toupper((unsigned char)ca); + cb = toupper((unsigned char)cb); + } + + if (ca < cb) + return -1; + + if (ca > cb) + return +1; + + ++ai; ++bi; + } +} + + +int +strnatcmp(char const *a, char const *b) { + return strnatcmp0(a, b, 0); +} + + +/* Compare, recognizing numeric string and ignoring case. */ +int +strnatcasecmp(char const *a, char const *b) { + return strnatcmp0(a, b, 1); +} --- util.c Mon Oct 18 13:14:58 2021 +++ util.c Tue Oct 19 11:04:52 2021 @@ -0,0 +1,577 @@ +/* + * Copyright (c) 2020-2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "util.h" + +#define ERROR_ALERT_ID 129 +#define ERROR_STRING_SIZE 1024 + +enum { + STOP_ALERT, + CAUTION_ALERT, + NOTE_ALERT +}; + +Handle err_str; +static TEHandle track_control_te = NULL; + +void vwarn(short alert_func, const char *format, va_list ap); + +void +err_init(void) +{ + if (!(err_str = NewHandle(ERROR_STRING_SIZE))) { + SysBeep(20); + ExitToShell(); + } +} + +void * +xmalloc(size_t size) +{ + void *ptr; + + if (size == 0) + err(2, "xmalloc: zero size"); + ptr = malloc(size); + if (ptr == NULL) + err(2, "xmalloc: allocating %zu bytes", size); + return ptr; +} + +void * +xmalloczero(size_t size) +{ + void *ptr = xmalloc(size); + memset(ptr, 0, size); + return ptr; +} + +void * +xcalloc(size_t nmemb, size_t size) +{ + void *ptr; + + ptr = calloc(nmemb, size); + if (ptr == NULL) + err(2, "xcalloc: allocating %zu * %zu bytes", nmemb, size); + return ptr; +} + +void * +xrealloc(void *src, size_t size) +{ + void *ret; + + ret = realloc(src, size); + if (ret == NULL) + err(2, "Couldn't realloc %lu bytes of memory", size); + return ret; +} + +#define MUL_NO_OVERFLOW ((size_t)1 << (sizeof(size_t) * 4)) + +void * +xreallocarray(void *optr, size_t nmemb, size_t size) +{ + void *new_ptr; + + if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && + nmemb > 0 && SIZE_MAX / nmemb < size) { + err(2, "reallocarray"); + } + if ((new_ptr = realloc(optr, size * nmemb)) == NULL) + err(2, "realloc"); + + return new_ptr; +} + +char * +xstrdup(const char *str) +{ + char *cp; + size_t len; + + len = strlen(str); + + cp = xmalloc(len + 1); + strncpy(cp, str, len); + cp[len] = '\0'; + + return cp; +} + +void +vwarn(short alert_func, const char *format, va_list ap) +{ + size_t len; + short quit = 0; + WindowPtr win; + + GetPort(&win); + + HLock(err_str); + len = vsprintf(*err_str, format, ap); + if (len >= ERROR_STRING_SIZE) { + sprintf(*err_str, "raise_error string overflow!"); + quit = 1; + } + + ParamText(CtoPstr(*err_str), "\p", "\p", "\p"); + switch (alert_func) { + case CAUTION_ALERT: + CautionAlert(ERROR_ALERT_ID, nil); + break; + case NOTE_ALERT: + NoteAlert(ERROR_ALERT_ID, nil); + break; + default: + StopAlert(ERROR_ALERT_ID, nil); + } + HUnlock(err_str); + + SetPort(win); + + if (quit) + ExitToShell(); +} + +void +warn(const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + vwarn(CAUTION_ALERT, format, ap); + va_end(ap); +} + +void +warnx(const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + vwarn(CAUTION_ALERT, format, ap); + va_end(ap); +} + +void +err(short retcode, const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + vwarn(STOP_ALERT, format, ap); + va_end(ap); + + ExitToShell(); +} + +void +note(const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + vwarn(NOTE_ALERT, format, ap); + va_end(ap); +} + +Handle +xNewHandle(unsigned long size) +{ + Handle h; + + h = NewHandle(size); + if (h == NULL) + err(1, "Failed to NewHandle(%lu)", size); + + return h; +} + +Handle +xGetResource(ResType type, short id) +{ + Handle h; + + h = GetResource(type, id); + if (h == NULL) + err(1, "Failed to find resource %d", id); + + return h; +} + +StringHandle +xGetString(short id) +{ + StringHandle h; + + h = GetString(id); + if (h == NULL) + err(1, "Failed to find STR resource %d", id); + + return h; +} + +/* + * With SFGetFile, when the user has a folder selected and clicks Open, + * File Manager will change to the folder and expect the user to select a + * file in it. This triggers sfHookOpenFolder, but when the user double- + * clicks on a folder to browse into it, the same result code is returned. + * + * To be able to return the folder itself, we need to know whether the user + * double-clicked a folder, or clicked Open. When we get our hook callback, + * we check where the mouse is in relation to the Open button rect. If + * the user clicked Open, we return the folder itself, otherwise we assume + * the user double-clicked a folder and we should browse into it. + * + * Finally, we return the full path of the file/folder as a char *, rather + * than just a ref id. + */ +char * +askfileordirpath(void) +{ + Point pt = { 75, 75 }; + SFReply reply; + HParamBlockRec hpbr; + Str63 fName = { 0 }; + char *ret = NULL; + + SFGetFile(pt, "\p", NULL, -1, 0, open_dialog_hook, &reply); + if (!reply.good) + return NULL; + + if (reply.fName[0] == 0) { + /* selected a folder, look it up */ + hpbr.fileParam.ioCompletion = 0; + hpbr.fileParam.ioNamePtr = (StringPtr)&fName; + hpbr.fileParam.ioVRefNum = reply.vRefNum; + hpbr.fileParam.ioDirID = reply.fType; + hpbr.fileParam.ioFDirIndex = -1; + PBGetCatInfo(&hpbr, false); + memcpy(reply.fName, fName, sizeof(fName)); + } + + getpath(reply.vRefNum, reply.fName, &ret, true); + + return ret; +} + +pascal short +open_dialog_hook(short theItem, DialogPtr theDialog) +{ + short item_type; + Handle item; + Rect item_rect; + Point mouse; + + if (theItem == sfHookOpenFolder) { + GetDItem(theDialog, getOpen, &item_type, &item, &item_rect); + GetMouse(&mouse); + + if (PtInRect(mouse, &item_rect)) { + /* clicked open */ + return getOpen; + } + } + + return theItem; +} + +short +getpath(short vRefNum, Str255 fileName, char **ret, short include_file) +{ + WDPBRec wdir; + HVolumeParam wvol; + DirInfo wcinfo; + Str255 name; + size_t retlen = 0; + char *tmp; + + wdir.ioVRefNum = wdir.ioWDVRefNum = vRefNum; + wdir.ioWDIndex = 0; + wdir.ioWDProcID = 0; + wdir.ioNamePtr = (StringPtr)&name; + if (PBGetWDInfo(&wdir, 0) != noErr) { + warn("Failed looking up directory"); + return 1; + } + + wvol.ioNamePtr = (StringPtr)&name; + wvol.ioVRefNum = vRefNum; + wvol.ioVolIndex = 0; + + if (PBHGetVInfoSync((HParmBlkPtr)&wvol) != noErr) { + warn("Failed getting volume info"); + return 1; + } + + if (wvol.ioVSigWord != 0x4244) { + warn("Unknown filesystem type 0x%x", wvol.ioVSigWord); + return 1; + } + + wcinfo.ioNamePtr = (StringPtr)&name; + wcinfo.ioVRefNum = vRefNum; + wcinfo.ioFDirIndex = -1; + wcinfo.ioDrParID = wdir.ioWDDirID; + wcinfo.ioDrDirID = wdir.ioWDDirID; + + /* go backwards, prepending each folder's parent */ + while (wcinfo.ioDrParID != 1) { + wcinfo.ioDrDirID = wcinfo.ioDrParID; /* .. */ + + if (PBGetCatInfo((CInfoPBPtr)&wcinfo, 0) != noErr) + break; + + if (retlen == 0) { + retlen = name[0]; + *ret = xmalloc(retlen + 1); + sprintf(*ret, "%s", PtoCstr(name)); + } else { + tmp = xstrdup(*ret); + free(*ret); + *ret = xmalloc(retlen + 1 + name[0] + 1); + retlen += 1 + name[0]; + sprintf(*ret, "%s:%s", PtoCstr(name), tmp); + free(tmp); + } + } + + if (include_file) { + /* append the original path */ + memcpy(name, fileName, sizeof(name)); + PtoCstr(name); + if (retlen == 0) + *ret = xstrdup((char *)name); + else { + *ret = xrealloc(*ret, retlen + 1 + fileName[0] + 1); + sprintf(*ret + retlen, ":%s", (char *)name); + } + } else if (retlen == 0) { + *ret = NULL; + } + + return 0; +} + +short +stat(const char *path, struct stat *sb) +{ + CInfoPBRec catblock = { 0 }; + short ret; + char *tpath; + + tpath = xstrdup(path); + CtoPstr(tpath); + + catblock.hFileInfo.ioNamePtr = (StringPtr)tpath; + ret = PBGetCatInfo(&catblock, 0); + free(tpath); + if (ret != noErr) + return -1; + + /* data fork logical length + resource fork logical length */ + sb->st_size = catblock.hFileInfo.ioFlLgLen + + catblock.hFileInfo.ioFlRLgLen; + sb->st_ctime = catblock.hFileInfo.ioFlCrDat; + sb->st_mtime = catblock.hFileInfo.ioFlMdDat; + sb->st_flags = catblock.hFileInfo.ioFlAttrib; + + return 0; +} + +short +is_dir(char *path) +{ + struct stat st; + short ret; + + if ((ret = stat(path, &st)) != 0) + return ret; + + /* bit 4 is set in ioFlAttrib if the item is a directory */ + if (st.st_flags & (1 << 4)) + return 1; + + return 0; +} + +short +FontHeight(short font_id, short size) +{ + FMInput f_in; + FMOutput *f_out; + + if (font_id == -1) + font_id = thePort->txFont; + if (size == -1) + size = thePort->txSize; + + f_in.family = font_id; + f_in.size = size; + f_in.face = 0; + f_in.needBits = false; + f_in.device = 0; + f_in.numer.h = f_in.numer.v = 1; + f_in.denom.h = f_in.denom.v = 1; + + f_out = FMSwapFont(&f_in); + + return (((f_out->leading + f_out->ascent + f_out->descent) * + f_out->numer.v) / f_out->denom.v); +} + +void +DrawGrowIconOnly(WindowPtr win) +{ + Rect r; + RgnHandle tmp; + + GetClip(tmp = NewRgn()); + r = win->portRect; + r.top = r.bottom - SCROLLBAR_WIDTH; + r.left = r.right - SCROLLBAR_WIDTH + 1; + ClipRect(&r); + DrawGrowIcon(win); + SetClip(tmp); + DisposeRgn(tmp); +} + +void +UpdateScrollbarForTE(ControlHandle scroller, TEHandle te) +{ + size_t vlines; + TERec *ter; + + HLock(te); + ter = *te; + + vlines = (ter->viewRect.bottom - ter->viewRect.top) / ter->lineHeight; + if (vlines >= ter->nLines) + vlines = ter->nLines; + SetCtlMax(scroller, ter->nLines - vlines + 1); + SetCtlValue(scroller, 1); + + HUnlock(te); +} + +void +SetTrackControlTE(TEHandle te) +{ + track_control_te = te; +} + +pascal void +TrackMouseDownInControl(ControlHandle control, short part) +{ + short page, val, adj; + + if (track_control_te == NULL) + err(1, "TrackMouseDownInControl without SetTrackControlTE"); + + /* keep 1 line of context between pages */ + page = (((*track_control_te)->viewRect.bottom - + (*track_control_te)->viewRect.top) / + (*track_control_te)->lineHeight) - 1; + + adj = 0; + switch (part) { + case inUpButton: + adj = -1; + break; + case inPageUp: + adj = -page; + break; + case inDownButton: + adj = 1; + break; + case inPageDown: + adj = page; + break; + } + + val = GetCtlValue(control); + if (val + adj < GetCtlMin(control)) + adj = -(val - GetCtlMin(control)); + if (val + adj > GetCtlMax(control)) + adj = (GetCtlMax(control) - val); + if (adj == 0) + return; + + TEScroll(0, -adj * (*track_control_te)->lineHeight, track_control_te); + SetCtlValue(control, val + adj); +} + +short +TECanAddLine(TEHandle teh, size_t addition) +{ + TERec *te; + short ret = 1; + + HLock(teh); + te = *teh; + + if ((long)te->teLength + addition > 32767) + ret = 0; + else if ((long)te->nLines * (long)te->lineHeight > 32767) + ret = 0; + + HUnlock(teh); + return ret; +} + +void +TECopyRemovingFakeTabs(TEHandle teh) +{ + TERec *te; + char *sel, *tetext; + short len, pos, i; + char c; + + HLock(teh); + te = *teh; + len = te->selEnd - te->selStart; + if (len < 1) { + HUnlock(teh); + return; + } + + HLock(te->hText); + tetext = *(te->hText); + sel = xmalloc(len); + pos = 0; + for (i = 0; i < len; i++) { + c = tetext[te->selStart + i]; + if (c == '\0' && tetext[te->selStart + i + 1] == '\t') + /* fake tab, skip both */ + i++; + else + sel[pos++] = c; + } + + ZeroScrap(); + PutScrap(pos, 'TEXT', sel); + free(sel); + HUnlock(te->hText); + HUnlock(teh); +} --- util.h Mon Oct 18 13:09:59 2021 +++ util.h Tue Oct 19 11:03:31 2021 @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020-2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef __UTIL_H__ +#define __UTIL_H__ + +#include <stdlib.h> +#include <limits.h> +#include <time.h> + +#ifndef SIZE_MAX +#define SIZE_MAX ULONG_MAX +#endif + +#define SCROLLBAR_WIDTH 16 + +#define nitems(what) (sizeof((what)) / sizeof((what)[0])) + +typedef signed long off_t; +typedef signed long ssize_t; +typedef unsigned char u_char; +typedef unsigned long u_int; + +typedef struct { + short push[2], rts; + void *addr; +} tCodeStub; + +typedef struct stat { + short st_mode; + ssize_t st_size; + time_t st_ctime; + time_t st_mtime; + unsigned char st_flags; +}; + +void err_init(void); +void *xmalloc(size_t); +void *xmalloczero(size_t); +void *xcalloc(size_t, size_t); +void *xrealloc(void *src, size_t size); +void *xreallocarray(void *, size_t, size_t); +char *xstrdup(const char *); + +void warnx(const char *format, ...); +void warn(const char *format, ...); +void err(short retcode, const char *format, ...); +void note(const char *format, ...); + +int strnatcmp(char const *a, char const *b); +int strnatcasecmp(char const *a, char const *b); + +Handle xNewHandle(unsigned long size); +Handle xGetResource(ResType type, short id); +StringHandle xGetString(short id); +char *xGetStringAsChar(short id); +long xGetStringAsLong(short id); + +char *askfileordirpath(void); +short getpath(short vRefNum, Str255 fileName, char **ret, + short include_file); +pascal short open_dialog_hook(short theItem, DialogPtr theDialog); +pascal Boolean open_dialog_filter(DialogPtr theDialog, + EventRecord *theEvent, short *itemHit); +short is_dir(char *path); +short stat(const char *path, struct stat *sb); + +short FontHeight(short font_id, short size); +void DrawGrowIconOnly(WindowPtr win); +void UpdateScrollbarForTE(ControlHandle scroller, TEHandle te); +void SetTrackControlTE(TEHandle te); +pascal void TrackMouseDownInControl(ControlHandle control, short part); +void TECopyRemovingFakeTabs(TEHandle teh); +short TECanAddLine(TEHandle teh, size_t addition); + +Handle SizedTENew(Rect viewRect, unsigned long size); + + +#endif