From e094afc24575cbff19f2fae8a372082fc6aa883a Mon Sep 17 00:00:00 2001 From: Tyler Coffman Date: Thu, 28 Jul 2022 16:40:36 -0700 Subject: [PATCH] feat: Add new 'last-edit' tour versioning scheme. The Current Commit versioning strategy for tours is quite brittle, because it is based on commit hashes. The moment history gets rewritten by a rebase, the tour becomes untethered from the commit that we are trying to attach it to, because the commit hash that's recorded in the tour file no longer exists. Also, the Current Commit versioning strategy means that a tour commit must be in a separate commit, becuase the commit that it's attached to must already exist so that its hash can be recoreded in the `ref` field of the tour. Introduce a 'last-edit' versioning scheme. If the value of the ref is `'last-edit'`, then the tour is associated with the most recent commit that modifies the tour file. This will enable a code change and a tour which documents the code change to exist in the same commit. If during code review, the git history is rewritten by a rebase, the tour will still properly be associated with the correct commit. This should provide a far more robust and simpler user experience than the Current Commit versioning strategy. --- README.md | 3 ++- src/git.ts | 17 +++++++++++++++++ src/notebook/index.ts | 2 +- src/player/index.ts | 2 +- src/recorder/commands.ts | 7 +++++++ src/store/actions.ts | 2 +- src/utils.ts | 25 +++++++++++++++++++++---- 7 files changed, 50 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 46fafad..4ebf891 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ In order to make it simpler to call common commands, CodeTour will prompt you wi When you record a tour, you'll be asked which git "ref" to associate it with. This allows you to define how resilient you want the tour to be, as changes are made to the respective codebase. - + You can choose to associate with the tour with the following ref types: @@ -183,6 +183,7 @@ You can choose to associate with the tour with the following ref types: - `Current Branch` - The tour is restricted to the current branch. This can have the same resiliency challenges as `None`, but, it allows you to maintain a special branch for your tours that can be versioned seperately. If the end-user has the associated branch checked out, then the tour will enable them to make edits to files as its taken. Otherwise, the tour will replay with read-only files. - `Current Commit` - The tour is restricted to the current commit, and therefore, will never get out of sync. If the end-user's `HEAD` points at the specified commit, then the tour will enable them to make edits to files as its taken. Otherwise, the tour will replay with read-only files. - Tags - The tour is restricted to the selected tag, and therefore, will never get out of sync. The repo's entire list of tags will be displayed, which allows you to easily select one. +- `Last Commit that modified the tour` - The tour is restricted to the most recent commit that modified the tour file. If the end-user's `HEAD` points at the specified commit, then the tour will enable them to make edits to files as its taken. Otherwise, the tour will replay with read-only files. This allows you to bundle a code tour along with the changes that it's explaining into a single git commit. At any time, you can edit the tour's ref by right-clicking it in the `CodeTour` tree and selecting `Change Git Ref`. This let's you "rebase" a tour to a tag/commit as you change/update your code and/or codebase. diff --git a/src/git.ts b/src/git.ts index 2ffc161..5026499 100644 --- a/src/git.ts +++ b/src/git.ts @@ -21,8 +21,25 @@ export interface RepositoryState { readonly refs: Ref[]; } +export interface LogOptions { + /** Max number of log entries to retrieve. If not specified, the default is 32. */ + readonly maxEntries?: number; + readonly path?: string; +} + +export interface Commit { + readonly hash: string; + readonly message: string; + readonly parents: string[]; + readonly authorDate?: Date; + readonly authorName?: string; + readonly authorEmail?: string; + readonly commitDate?: Date; +} + export interface Repository { readonly state: RepositoryState; + log(options?: LogOptions): Promise; } interface GitAPI { diff --git a/src/notebook/index.ts b/src/notebook/index.ts index 439c080..20ff7cf 100644 --- a/src/notebook/index.ts +++ b/src/notebook/index.ts @@ -21,7 +21,7 @@ class CodeTourNotebookProvider implements vscode.NotebookSerializer { let steps: any[] = []; for (let item of tour.steps) { - const uri = await getStepFileUri(item, workspaceRoot, tour.ref); + const uri = await getStepFileUri(item, workspaceRoot, tour); const document = await vscode.workspace.openTextDocument(uri); const startLine = item.line! > 10 ? item.line! - 10 : 0; diff --git a/src/player/index.ts b/src/player/index.ts index fa85659..59d3e79 100644 --- a/src/player/index.ts +++ b/src/player/index.ts @@ -242,7 +242,7 @@ async function renderCurrentStep() { } const workspaceRoot = store.activeTour?.workspaceRoot; - const uri = await getStepFileUri(step, workspaceRoot, currentTour.ref); + const uri = await getStepFileUri(step, workspaceRoot, currentTour); let line = step.line ? step.line - 1 diff --git a/src/recorder/commands.ts b/src/recorder/commands.ts index 54a008c..38320ab 100644 --- a/src/recorder/commands.ts +++ b/src/recorder/commands.ts @@ -872,6 +872,13 @@ export function registerRecorderCommands() { description: "Keep the tour associated with a specific commit", ref: repository.state.HEAD ? repository.state.HEAD.commit! : "", alwaysShow: true + }, + { + label: "$(git-commit) Last commit that modified the tour", + description: + "Keep the tour associated with the most recent commit that modified the tour", + ref: "last-edit", + alwaysShow: true } ]; diff --git a/src/store/actions.ts b/src/store/actions.ts index f70a462..facc13a 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -220,7 +220,7 @@ export async function exportTour(tour: CodeTour) { } const workspaceRoot = getWorkspaceUri(tour); - const stepFileUri = await getStepFileUri(step, workspaceRoot, tour.ref); + const stepFileUri = await getStepFileUri(step, workspaceRoot, tour); const contents = await readUriContents(stepFileUri); delete step.markerTitle; diff --git a/src/utils.ts b/src/utils.ts index 3fef5ed..2eff1b0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -69,8 +69,9 @@ export function getFileUri(file: string, workspaceRoot?: Uri) { export async function getStepFileUri( step: CodeTourStep, workspaceRoot?: Uri, - ref?: string + tour?: CodeTour ): Promise { + const ref = tour?.ref; let uri; if (step.contents) { uri = Uri.parse(`${FS_SCHEME}://current/${step.file}`); @@ -79,10 +80,26 @@ export async function getStepFileUri( ? Uri.parse(step.uri) : getFileUri(step.file!, workspaceRoot); - if (api && ref && ref !== "HEAD") { + if (api && tour && ref && ref !== "HEAD") { const repo = api.getRepository(uri); - if ( + if (ref === "last-edit") { + const tourUri = Uri.parse(tour.id); + const tourPath = tourUri.path; + + if (repo && repo.state.HEAD) { + const logResults = await repo.log({ + maxEntries: 1, + path: tourPath + }); + if (logResults.length > 0) { + const commit = logResults[0]; + if (repo.state.HEAD.commit !== commit.hash) { + uri = await api.toGitUri(uri, commit.hash); + } + } + } + } else if ( repo && repo.state.HEAD && repo.state.HEAD.name !== ref && // The tour refs the user's current branch @@ -184,7 +201,7 @@ async function updateMarkerTitleForStep(tour: CodeTour, stepNumber: number) { const uri = await getStepFileUri( tour.steps[stepNumber], getWorkspaceUri(tour), - tour.ref + tour ); const document = await workspace.openTextDocument(uri);