feat(git): fetch/push/ff primitives with auth env, no task trailer in company repos
This commit is contained in:
@@ -450,6 +450,45 @@ public sealed class GitService
|
|||||||
return (exitCode, stderr);
|
return (exitCode, stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>git fetch origin</c> for one branch into <c>refs/remotes/origin/<branch></c>. The explicit
|
||||||
|
/// refspec works without a configured fetch refspec. <paramref name="env"/> carries the auth
|
||||||
|
/// header (never the command line) and must disable the terminal prompt.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(int ExitCode, string Stderr)> FetchBranchAsync(
|
||||||
|
string repoDir, string branch, IReadOnlyDictionary<string, string>? env, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
|
||||||
|
["fetch", "origin", $"+refs/heads/{branch}:refs/remotes/origin/{branch}"], ct, env: env);
|
||||||
|
return (exitCode, stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Moves the local, not checked-out <paramref name="branch"/> to <c>origin/<branch></c>; git itself refuses a non-fast-forward. No network.</summary>
|
||||||
|
public async Task<(int ExitCode, string Stderr)> FastForwardLocalBranchAsync(
|
||||||
|
string repoDir, string branch, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
|
||||||
|
["fetch", ".", $"refs/remotes/origin/{branch}:refs/heads/{branch}"], ct);
|
||||||
|
return (exitCode, stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary><c>git push -u origin <branch></c> from <paramref name="worktreePath"/>; auth via <paramref name="env"/>.</summary>
|
||||||
|
public async Task<(int ExitCode, string Stderr)> PushBranchAsync(
|
||||||
|
string worktreePath, string branch, IReadOnlyDictionary<string, string>? env, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, _, stderr) = await RunGitAsync(worktreePath, ["push", "-u", "origin", branch], ct, env: env);
|
||||||
|
return (exitCode, stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Branch that <c>refs/remotes/origin/HEAD</c> points to (without <c>origin/</c>), or null.</summary>
|
||||||
|
public async Task<string?> GetRemoteHeadBranchAsync(string repoDir, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], ct);
|
||||||
|
if (exitCode != 0 || string.IsNullOrWhiteSpace(stdout)) return null;
|
||||||
|
var head = stdout.Trim();
|
||||||
|
return head.StartsWith("origin/", StringComparison.Ordinal) ? head["origin/".Length..] : head;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary><c>git merge --ff-only</c>: moves the checked-out branch to <paramref name="sourceRef"/> or fails without touching it.</summary>
|
/// <summary><c>git merge --ff-only</c>: moves the checked-out branch to <paramref name="sourceRef"/> or fails without touching it.</summary>
|
||||||
public async Task<(int ExitCode, string Stderr)> MergeFastForwardOnlyAsync(
|
public async Task<(int ExitCode, string Stderr)> MergeFastForwardOnlyAsync(
|
||||||
string worktreePath, string sourceRef, CancellationToken ct = default)
|
string worktreePath, string sourceRef, CancellationToken ct = default)
|
||||||
@@ -721,7 +760,8 @@ public sealed class GitService
|
|||||||
|
|
||||||
|
|
||||||
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync(
|
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync(
|
||||||
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null, bool trimOutput = true)
|
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null, bool trimOutput = true,
|
||||||
|
IReadOnlyDictionary<string, string>? env = null)
|
||||||
{
|
{
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
@@ -742,6 +782,8 @@ public sealed class GitService
|
|||||||
// of the OS locale git for Windows picks up.
|
// of the OS locale git for Windows picks up.
|
||||||
psi.Environment["LC_ALL"] = "C";
|
psi.Environment["LC_ALL"] = "C";
|
||||||
psi.Environment["LANGUAGE"] = "C";
|
psi.Environment["LANGUAGE"] = "C";
|
||||||
|
if (env is not null)
|
||||||
|
foreach (var (key, value) in env) psi.Environment[key] = value;
|
||||||
psi.ArgumentList.Add("-C");
|
psi.ArgumentList.Add("-C");
|
||||||
psi.ArgumentList.Add(workDir);
|
psi.ArgumentList.Add(workDir);
|
||||||
foreach (var a in args) psi.ArgumentList.Add(a);
|
foreach (var a in args) psi.ArgumentList.Add(a);
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ namespace ClaudeDo.Worker.Runner;
|
|||||||
|
|
||||||
public static class CommitMessageBuilder
|
public static class CommitMessageBuilder
|
||||||
{
|
{
|
||||||
public static string Build(string commitType, string listName, string taskTitle, string? taskDescription, string taskId)
|
/// <summary>Task commit message; <paramref name="includeTrailer"/> is false in company repos, whose convention forbids signature trailers.</summary>
|
||||||
|
public static string Build(string commitType, string listName, string taskTitle, string? taskDescription, string taskId,
|
||||||
|
bool includeTrailer = true)
|
||||||
{
|
{
|
||||||
var slug = ToSlug(listName);
|
var slug = ToSlug(listName);
|
||||||
var title = Truncate(taskTitle, 60);
|
var title = Truncate(taskTitle, 60);
|
||||||
@@ -22,9 +24,11 @@ public static class CommitMessageBuilder
|
|||||||
sb.Append(Truncate(taskDescription!.Trim(), 400));
|
sb.Append(Truncate(taskDescription!.Trim(), 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trailer is always included.
|
if (includeTrailer)
|
||||||
sb.Append("\n\n");
|
{
|
||||||
sb.Append($"ClaudeDo-Task: {taskId}");
|
sb.Append("\n\n");
|
||||||
|
sb.Append($"ClaudeDo-Task: {taskId}");
|
||||||
|
}
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,8 +199,10 @@ public sealed class WorktreeManager
|
|||||||
|
|
||||||
await _git.AddAllAsync(ctx.WorktreePath, ct);
|
await _git.AddAllAsync(ctx.WorktreePath, ct);
|
||||||
|
|
||||||
|
var isCompany = list.WorkingDir is not null
|
||||||
|
&& await CompanyRepo.IsCompanyAsync(_git, list.WorkingDir, _cfg.CompanyRemotePattern, ct);
|
||||||
var message = CommitMessageBuilder.Build(
|
var message = CommitMessageBuilder.Build(
|
||||||
task.CommitType, list.Name, task.Title, task.Description, task.Id);
|
task.CommitType, list.Name, task.Title, task.Description, task.Id, includeTrailer: !isCompany);
|
||||||
|
|
||||||
await _git.CommitAsync(ctx.WorktreePath, message, ct);
|
await _git.CommitAsync(ctx.WorktreePath, message, ct);
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ public class CommitMessageBuilderTests
|
|||||||
Assert.Equal("ClaudeDo-Task: id-456", lines[4]);
|
Assert.Equal("ClaudeDo-Task: id-456", lines[4]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithoutTrailer_CompanyRepoCommitHasNoClaudeDoTaskLine()
|
||||||
|
{
|
||||||
|
var msg = CommitMessageBuilder.Build("feat", "Lager App", "add scan", "Detailed description here", "id-456",
|
||||||
|
includeTrailer: false);
|
||||||
|
|
||||||
|
Assert.Equal("feat(lager-app): add scan\n\nDetailed description here", msg);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BuildMerge_ConventionalHeaderWithMergeVerbAndTrailer()
|
public void BuildMerge_ConventionalHeaderWithMergeVerbAndTrailer()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user