Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions .eslintrc.json

This file was deleted.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules
.vscode-test/
*.vsix
*.roo
*.aider*
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
1 change: 0 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
enable-pre-post-scripts = true
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,30 @@

🔍**The Lowdown**: Ever wonder who altered that crucial line of code and when? With Git Search, you're just a few clicks away from unveiling the mysteries of your codebase. Just cross your fingers you're not the digital detective hunting your own coding missteps! 😉

![Example](./assets/out.gif)
## Demo

🌟 **What's Git Search?**
![Git Search in Action](./assets/out.gif)

## Why Git Search?

Working with legacy code or unfamiliar projects often presents a host of challenges, particularly when trying to decipher what's happening and what has transpired. Frequently, I find that commit messages are either missing or lack sufficient detail. In the best-case scenario, they might point to a Jira ticket, but as we know, that doesn't always shed much light on the issue. In the worst cases, I encounter unhelpful commit messages like "fix."

Regularly, there's a need to understand how a specific variable or function was created or used. Typically, this would involve using git log -S in the terminal and manually opening each commit to examine the changes. This process can be quite tedious. To streamline this task, I created this extension.

🎸 **Why It's a Game Changer**:

**Rapid Git Log Searches**: Dive into your code's history with the speed of a hot rod. Unearth the "who" and "when" behind every change, fast.

**Direct Commit Access**: Found something intriguing? Jump straight from your search results to the actual commit in your remote repository.

**Smart Pagination**: Dealing with a mountain of results? Effortlessly navigate through them with intuitive pagination.
## Features

**Seamless Repo Integration**: Git Search tunes itself to your current workspace's Git setup. No complex configurations, just plug and play.
- **🚀 Rapid Git Log Searches**: Dive into your code's history with the speed of a hot rod. Unearth the "who" and "when" behind every change, fast.
- **🔗 Direct Commit Access**: Found something intriguing? Jump straight from your search results to the actual commit in your remote repository.
- **📄 Smart Pagination**: Dealing with a mountain of results? Effortlessly navigate through them with intuitive pagination.
- **⚙️ Seamless Repo Integration**: Git Search tunes itself to your current workspace's Git setup. No complex configurations, just plug and play.

🔥 **Getting Started**:
## How to Use

Launch the command palette and search for 'Show Git Search Panel'.
Enter your query, hit enter, and watch as Git Search works its magic.Browse through the results, click on commit links for the full story, or keep the investigation going with 'Load More'.
1. Open the Command Palette (`Cmd+Shift+P` on macOS or `Ctrl+Shift+P` on Windows/Linux).
2. Search for and select the **"Show Git Search Panel"** command.
3. In the new panel, type your search term into the input box and press Enter. This will run a `git log -S"<your-term>"` command to find commits that introduce or remove that string.
4. The results, including commit hash, author, and date, will be displayed in the panel.

🤝 **Join the Mission**:
## Contributing

Got some cool ideas or valuable feedback? Team up with us on [GitHub](https://github.com/lmn451/git-search) and help make Git Search even more awesome. Let’s code, collaborate, and create something phenomenal!
46 changes: 46 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const globals = require("globals");
const js = require("@eslint/js");

module.exports = [
// It's a good practice to start with ESLint's recommended rules.
js.configs.recommended,

{
// This configuration object applies to all files.
languageOptions: {
ecmaVersion: 2018,
// The project uses CommonJS modules (require/module.exports).
sourceType: "commonjs",
parserOptions: {
ecmaFeatures: {
// This was enabled in the old config, keeping for parity.
jsx: true,
},
},
// Define the global variables available in the project.
globals: {
...globals.node, // Globals for Node.js environment
...globals.mocha, // Globals for Mocha testing framework
},
},

// Define custom rules. These are ported from the old .eslintrc.json.
rules: {
"no-const-assign": "warn",
"no-this-before-super": "warn",
"no-undef": "warn",
"no-unreachable": "warn",
"no-unused-vars": "warn",
"constructor-super": "warn",
"valid-typeof": "warn",
},

// Specify files and directories to be ignored by ESLint.
ignores: [
"node_modules/",
"dist/",
"*.vsix", // Ignore packaged extension files
"assets/", // Ignore binary assets
],
},
];
240 changes: 26 additions & 214 deletions extension.js
Original file line number Diff line number Diff line change
@@ -1,228 +1,40 @@
const {
getRelatedCommitsInfo,
getDiff,
getRepoUrl,
} = require("./src/gitCommands");
const vscode = require("vscode");
const fs = require("fs");
const path = require("path");
const Convert = require("ansi-to-html");
const { adjustDate, formatDate } = require("./src/helpers");
const { highlightQueryInHtml, escapeHtml } = require("./src/htmlHelpers");

const convert = new Convert({
colors: [
"#000000", // Black
"#DB7093", // Red
// "#00FF00", // Green
],
stream: true,
});

let PAGE_SIZE = 10;
let MODE = "S";
let NUMBER_OF_CONTEXT_LINES = 3;
let latestQuery = "";
let isLoadMore = false;
let lastCommitDate = "";
let currentCommits = [];
let redraw = false;

const getWorkspace = () => {
try {
return vscode.workspace.workspaceFolders[0].uri.fsPath;
} catch (err) {
return null;
}
};
const { createOrShow } = require("./src/webviewManager");

/**
* This method is called when your extension is activated.
* Your extension is activated the very first time the command is executed.
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
let disposable = vscode.commands.registerCommand("git-search.showPanel", () =>
showPanel(context)
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
const disposable = vscode.commands.registerCommand(
"git-search.showPanel",
() => {
// The code you place here will be executed every time your command is executed
createOrShow(context.extensionUri);
},
);

context.subscriptions.push(disposable);
}

function showPanel(context) {
const panel = vscode.window.createWebviewPanel(
"gitSearch",
"Git Search",
vscode.ViewColumn.One,
{ enableScripts: true }
);

panel.webview.html = getWebviewContent();
panel.webview.onDidReceiveMessage(
(message) => handleWebviewMessage(message, panel),
undefined,
context.subscriptions
);
}

function handleWebviewMessage(message, panel) {
switch (message.command) {
case "search":
handleSearchCommand(message.text, panel);
break;
case "loadMore":
handleLoadMoreCommand(panel);
break;
case "reset":
handleResetCommand(panel);
break;
case "changeMode":
handleChangeMode(message.mode);
break;
case "updateNumberOfContextLines":
handleUpdateNumberOfContextLines(message, panel);
break;
}
}

async function handleUpdateNumberOfContextLines(message, panel) {
NUMBER_OF_CONTEXT_LINES = message.value;
lastCommitDate = "";
redraw = true;
isLoadMore = false;
await executeGitSearch(latestQuery, panel);
}

async function handleSearchCommand(query, panel) {
if (query !== latestQuery) {
currentCommits = [];
}
latestQuery = query;
isLoadMore = false;
lastCommitDate = "";
panel.webview.postMessage({ command: "showResults", text: "Loading" });
await executeGitSearch(query, panel);
}

async function handleLoadMoreCommand(panel) {
isLoadMore = true;
redraw = true;
await executeGitSearch(latestQuery, panel);
}

function handleResetCommand(panel) {
latestQuery = "";
isLoadMore = false;
lastCommitDate = "";
currentCommits = [];
panel.webview.postMessage({ command: "reset", text: "" });
}

function handleChangeMode(value) {
if (!(value === "G" || value === "S")) return;
MODE = value;
}

function getWebviewContent() {
const htmlFilePath = path.join(__dirname, "gitSearchPanel.html");
return fs.readFileSync(htmlFilePath, "utf8");
}

async function executeGitSearch(rawQuery, panel) {
const query = rawQuery.trim();
if (!query) {
return panel.webview.postMessage({
command: "showResults",
text: "",
});
}

try {
const workspaceFolderPath = getWorkspace();
if (!workspaceFolderPath)
return panel.webview.postMessage({
command: "showResults",
text: `No workspace found`,
});
const repoUrl = await getRepoUrl(workspaceFolderPath);

if (!redraw || isLoadMore) {
const logOutput = await getRelatedCommitsInfo(
workspaceFolderPath,
query,
MODE,
lastCommitDate,
PAGE_SIZE
);
if (!logOutput)
return panel.webview.postMessage({
command: isLoadMore ? "appendResults" : "showResults",
text: null,
isLoadMore: false,
});

const commits = logOutput
.split("\n")
.map((line) => line.trim())
.filter(Boolean);

currentCommits.push(...commits);
lastCommitDate = adjustDate(commits.at(-1).split("|")[2]);
}

const diffPromises = currentCommits.map((commitEntry) => {
const [commitHash, author, commitDate] = commitEntry.split("|");
return getDiff(
workspaceFolderPath,
commitHash,
query,
NUMBER_OF_CONTEXT_LINES
)
.then((diffOutput) => ({ commitHash, diffOutput, commitDate, author }))
.catch((error) => {
vscode.window.showErrorMessage(error.stack);
return null; // Continue processing other commits
});
});

const diffResults = await Promise.all(diffPromises);
const contentArray = diffResults.map((diff) => {
if (!diff) return "";
const { commitHash, diffOutput, commitDate, author } = diff;
const highlightedDiff = highlightQueryInHtml(
escapeHtml(diffOutput),
escapeHtml(query)
);
const diffHtml = convert.toHtml(highlightedDiff);
return `<li class="commit-diff">Commit: <a href=${repoUrl}/commit/${commitHash}>${commitHash}</a> by ${author} at ${formatDate(
commitDate
)}<br><pre>${diffHtml}</pre></li>`;
});

let content = contentArray.join("");
panel.webview.postMessage({
command: redraw
? "showResults"
: isLoadMore
? "appendResults"
: "showResults",
text: content || "No results found",
latestQuery,
isLoadMore: contentArray ? contentArray.length == PAGE_SIZE : false,
});
} catch (error) {
vscode.window.showErrorMessage(error.stack);
panel.webview.postMessage({
command: "showResults",
text: error,
});
}
}

// This method is called when your extension is deactivated
function deactivate() {}

module.exports = {
activate,
deactivate,
handleSearchCommand,
handleLoadMoreCommand,
handleResetCommand,
handleWebviewMessage,
getWebviewContent,
executeGitSearch,
getTestApi: () => {
// This is a special export for testing purposes only.
if (process.env.VSCODE_TEST) {
const webviewManager = require("./src/webviewManager");
return {
getCurrentPanel: webviewManager.getCurrentPanel,
};
}
return null;
},
};
Loading