mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-06-04 10:19:29 +00:00
The regex captured the full fetch output line including the branch name. Before #4115, the command was built as a shell string, so the shell split the arguments correctly. After #4115 switched to `execFile`, the range and branch name were passed as a single argument (`60e0377..332e429 develop`) - which git rejects as ambiguous. The fix replaces the regex with column-based line parsing: find the line for the current branch, take the first column. Falls back to `<branch>..origin/<branch>` when fetch reports no changes (same behavior as before). Also adds unit tests for the new helper and corrects existing test snapshots that encoded the broken behavior. Fixes #4137.
219 lines
6.3 KiB
JavaScript
219 lines
6.3 KiB
JavaScript
const util = require("node:util");
|
|
const execFile = util.promisify(require("node:child_process").execFile);
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const Log = require("logger");
|
|
|
|
class GitHelper {
|
|
constructor () {
|
|
this.gitRepos = [];
|
|
this.gitResultList = [];
|
|
}
|
|
|
|
/**
|
|
* Extract commit range (<from>..<to>) for the current branch from `git fetch -n --dry-run` output.
|
|
* Falls back to `<branch>..origin/<branch>` when no matching update line is found.
|
|
* @param {string} fetchOutput fetch dry-run stderr output
|
|
* @param {string} currentBranch currently checked local branch
|
|
* @returns {string} commit range for rev-list
|
|
*/
|
|
getRefDiffFromFetchDryRun (fetchOutput, currentBranch) {
|
|
const fallbackRefDiff = `${currentBranch}..origin/${currentBranch}`;
|
|
|
|
for (const line of fetchOutput.split("\n")) {
|
|
const columns = line.trim().split(/\s+/);
|
|
const [refDiff, branchName] = columns;
|
|
|
|
if (branchName === currentBranch && refDiff?.includes("..")) {
|
|
return refDiff;
|
|
}
|
|
}
|
|
|
|
return fallbackRefDiff;
|
|
}
|
|
|
|
async execGit (moduleFolder, ...args) {
|
|
const { stdout = "", stderr = "" } = await execFile("git", args, { cwd: moduleFolder });
|
|
|
|
return { stdout, stderr };
|
|
}
|
|
|
|
async isGitRepo (moduleFolder) {
|
|
const { stderr } = await this.execGit(moduleFolder, "remote", "-v");
|
|
|
|
if (stderr) {
|
|
Log.error(`Failed to fetch git data for ${moduleFolder}: ${stderr}`);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async add (moduleName) {
|
|
let moduleFolder = `${global.root_path}`;
|
|
|
|
if (moduleName !== "MagicMirror") {
|
|
moduleFolder = `${moduleFolder}/modules/${moduleName}`;
|
|
}
|
|
|
|
try {
|
|
Log.info(`Checking git for module: ${moduleName}`);
|
|
// Throws error if file doesn't exist
|
|
fs.statSync(path.join(moduleFolder, ".git"));
|
|
|
|
// Fetch the git or throw error if no remotes
|
|
const isGitRepo = await this.isGitRepo(moduleFolder);
|
|
|
|
if (isGitRepo) {
|
|
// Folder has .git and has at least one git remote, watch this folder
|
|
this.gitRepos.push({ module: moduleName, folder: moduleFolder });
|
|
}
|
|
} catch {
|
|
// Error when directory .git doesn't exist or doesn't have any remotes
|
|
// This module is not managed with git, skip
|
|
}
|
|
}
|
|
|
|
async getStatusInfo (repo) {
|
|
let gitInfo = {
|
|
module: repo.module,
|
|
behind: 0, // commits behind
|
|
current: "", // branch name
|
|
hash: "", // current hash
|
|
tracking: "", // remote branch
|
|
isBehindInStatus: false
|
|
};
|
|
|
|
if (repo.module === "MagicMirror") {
|
|
// the hash is only needed for the mm repo
|
|
const { stderr, stdout } = await this.execGit(repo.folder, "rev-parse", "HEAD");
|
|
|
|
if (stderr) {
|
|
Log.error(`Failed to get current commit hash for ${repo.module}: ${stderr}`);
|
|
}
|
|
|
|
gitInfo.hash = stdout;
|
|
}
|
|
|
|
const { stderr, stdout } = await this.execGit(repo.folder, "status", "-sb");
|
|
|
|
if (stderr) {
|
|
Log.error(`Failed to get git status for ${repo.module}: ${stderr}`);
|
|
// exit without git status info
|
|
return;
|
|
}
|
|
|
|
// only the first line of stdout is evaluated
|
|
let status = stdout.split("\n")[0];
|
|
// examples for status:
|
|
// ## develop...origin/develop
|
|
// ## master...origin/master [behind 8]
|
|
// ## master...origin/master [ahead 8, behind 1]
|
|
// ## HEAD (no branch)
|
|
status = status.match(/## (.*)\.\.\.([^ ]*)(?: .*behind (\d+))?/);
|
|
// examples for status:
|
|
// [ '## develop...origin/develop', 'develop', 'origin/develop' ]
|
|
// [ '## master...origin/master [behind 8]', 'master', 'origin/master', '8' ]
|
|
// [ '## master...origin/master [ahead 8, behind 1]', 'master', 'origin/master', '1' ]
|
|
if (status) {
|
|
gitInfo.current = status[1];
|
|
gitInfo.tracking = status[2];
|
|
|
|
if (status[3]) {
|
|
// git fetch was already called before so `git status -sb` delivers already the behind number
|
|
gitInfo.behind = parseInt(status[3]);
|
|
gitInfo.isBehindInStatus = true;
|
|
}
|
|
}
|
|
|
|
return gitInfo;
|
|
}
|
|
|
|
async getRepoInfo (repo) {
|
|
const gitInfo = await this.getStatusInfo(repo);
|
|
|
|
if (!gitInfo || !gitInfo.current) {
|
|
return;
|
|
}
|
|
|
|
if (gitInfo.isBehindInStatus && (gitInfo.module !== "MagicMirror" || gitInfo.current !== "master")) {
|
|
return gitInfo;
|
|
}
|
|
|
|
// Git writes fetch dry-run updates to stderr.
|
|
const { stderr } = await this.execGit(repo.folder, "fetch", "-n", "--dry-run");
|
|
|
|
const refDiff = this.getRefDiffFromFetchDryRun(stderr, gitInfo.current);
|
|
|
|
// get behind with refs
|
|
try {
|
|
const { stdout } = await this.execGit(repo.folder, "rev-list", "--ancestry-path", "--count", refDiff);
|
|
gitInfo.behind = parseInt(stdout);
|
|
|
|
// for MagicMirror-Repo and "master" branch avoid getting notified when no tag is in refDiff
|
|
// so only releases are reported and we can change e.g. the README.md without sending notifications
|
|
if (gitInfo.behind > 0 && gitInfo.module === "MagicMirror" && gitInfo.current === "master") {
|
|
let tagList = "";
|
|
try {
|
|
const { stdout } = await this.execGit(repo.folder, "ls-remote", "-q", "--tags", "--refs");
|
|
tagList = stdout.trim();
|
|
} catch (err) {
|
|
Log.error(`Failed to get tag list for ${repo.module}: ${err}`);
|
|
}
|
|
// check if tag is between commits and only report behind > 0 if so
|
|
try {
|
|
const { stdout } = await this.execGit(repo.folder, "rev-list", "--ancestry-path", refDiff);
|
|
let cnt = 0;
|
|
for (const ref of stdout.trim().split("\n")) {
|
|
if (tagList.includes(ref)) cnt++; // tag found
|
|
}
|
|
if (cnt === 0) gitInfo.behind = 0;
|
|
} catch (err) {
|
|
Log.error(`Failed to get git revisions for ${repo.module}: ${err}`);
|
|
}
|
|
}
|
|
|
|
return gitInfo;
|
|
} catch (err) {
|
|
Log.error(`Failed to get git revisions for ${repo.module}: ${err}`);
|
|
}
|
|
}
|
|
|
|
async getRepos () {
|
|
this.gitResultList = [];
|
|
|
|
for (const repo of this.gitRepos) {
|
|
try {
|
|
const gitInfo = await this.getRepoInfo(repo);
|
|
|
|
if (gitInfo) {
|
|
this.gitResultList.push(gitInfo);
|
|
}
|
|
} catch (e) {
|
|
// Only log errors in non-test environments to keep test output clean
|
|
if (process.env.mmTestMode !== "true") {
|
|
Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.gitResultList;
|
|
}
|
|
|
|
checkUpdates () {
|
|
const updates = [];
|
|
|
|
for (const moduleInfo of this.gitResultList) {
|
|
if (moduleInfo.behind > 0 && moduleInfo.module !== "MagicMirror") {
|
|
Log.info(`Update found for module: ${moduleInfo.module}`);
|
|
updates.push(moduleInfo);
|
|
}
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
}
|
|
|
|
module.exports = GitHelper;
|