using System.Text;
using Octokit;
namespace AiAgent.Core.Infrastructure
{
public static class GitHubHelper
{
// === CONFIGURATION ===
// Replace these with your actual details.
private const string GitHubToken = "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";
private const string RepoOwner = "fredfesty"; // Your GitHub username
private const string RepoName = "NewsAI";
private const string Branch = "main"; // Or "master", depending on your repo
///
/// Creates and configures a new instance of the class.
///
/// The client is initialized with a custom product header value and authenticated using
/// the provided GitHub token. Ensure that the GitHub token is valid and has the necessary permissions for the
/// intended operations.
/// A configured instance ready for use.
private static GitHubClient GetClient()
{
var github = new GitHubClient(new ProductHeaderValue("My-CSharp-App"));
github.Credentials = new Credentials(GitHubToken);
return github;
}
///
/// Downloads a file from a GitHub repository and saves it to a specified local directory.
///
/// The file content is retrieved from the GitHub repository, decoded from Base64, and
/// written to the specified local directory. If the file already exists in the target directory, it will be
/// overwritten.
/// The relative path of the file within the repository. This path must be valid and point to an existing file.
/// The local directory where the file will be saved. The directory must exist, and the method will create a
/// file with the same name as the source file in this directory.
/// A representing the asynchronous operation. The task result contains the original
/// content of the downloaded file as a string.
/// Thrown if the file cannot be accessed in the repository, the file does not exist, or an error occurs during
/// the download or save operation.
public static async Task DownloadFile(string filePathInRepo, string localPath)
{
// Get the github client
var github = GetClient();
//string filePathInRepo = "src/uploaded-file.txt";
// We get the file's metadata, including its content (Base64 encoded) and SHA hash.
var file = await github.Repository.Content.GetAllContents(RepoOwner, RepoName, filePathInRepo)
?? throw new Exception($"Error: Unable to access the repository or path '{filePathInRepo}'.");
var targetFile = file.FirstOrDefault()
?? throw new Exception($"Error: The file '{filePathInRepo}' was not found.");
// The content is Base64 encoded, so we need to decode it.
//string originalFileContent = Encoding.UTF8.GetString(Convert.FromBase64String(targetFile.Content));
string fileName = Path.GetFileName(filePathInRepo);
string localFile = Path.Combine(localPath, fileName);
await File.WriteAllTextAsync(localFile, targetFile.Content);
return localFile;
}
///
/// Uploads a file to a specified path in the repository, updating the file if it already exists.
///
/// This method updates the file in the repository if it already exists at the specified
/// path. If the file does not exist, an exception is thrown. Ensure that the repository and branch are
/// accessible and that the provided paths are valid.
/// The path within the repository where the file should be uploaded. This must include the file name and
/// extension.
/// The local file path of the file to be uploaded. The file must exist at this path.
/// The commit message to associate with the file upload operation.
/// A task that represents the asynchronous operation. The task result contains the commit message of the
/// operation.
/// Thrown if the repository or specified path cannot be accessed, if the local file cannot be read, or if the
/// file update operation fails.
public static async Task UploadFile(string filePathInRepo, string localPath, string commitMessage)
{
// Get the github client
var github = GetClient();
// Try to get the existing file's metadata (if it exists)
var existingFile = await GetFileSha(github, filePathInRepo)
?? throw new Exception($"Error: Unable to access the repository or path '{filePathInRepo}'.");
// Read the content off the local file
string fileContent = await File.ReadAllTextAsync(localPath);
// Create a request to update the file
var updateFileRequest = new UpdateFileRequest(commitMessage, fileContent, existingFile.Sha, Branch)
?? throw new Exception("Error: Failed to create update request.");
// 4b. If the file exists, update it
var result = await github.Repository.Content.UpdateFile(RepoOwner, RepoName, filePathInRepo, updateFileRequest)
?? throw new Exception("Error: Failed to update the file in the repository.");
return result.Commit.Message;
}
///
/// Uploads a new file to the specified path in the repository.
///
/// This method reads the content of the specified local file and uploads it to the given
/// path in the repository. The file is created in the repository's default branch.
/// The path within the repository where the file will be created. This should include the file name and
/// extension.
/// The local file system path of the file to be uploaded. The file's content will be read from this path.
/// The commit message to associate with the file upload operation.
/// A representing the asynchronous operation. The task result contains the commit
/// message of the operation.
/// Thrown if the file upload request cannot be created or if the upload operation fails.
public static async Task UploadNewFile(string filePathInRepo, string localPath, string commitMessage)
{
// Get the github client
var github = GetClient();
// Read the content off the local file
string fileContent = await File.ReadAllTextAsync(localPath);
try
{
// First, try to get the file's metadata to see if it exists.
var existingFile = await github.Repository.Content.GetAllContents(RepoOwner, RepoName, filePathInRepo);
// If the file exists, we update it.
// The GetAllContents method returns a list; we need the SHA of the specific file.
var updateRequest = new UpdateFileRequest(commitMessage, fileContent, existingFile.First().Sha, Branch);
var updateResponse = await github.Repository.Content.UpdateFile(RepoOwner, RepoName, filePathInRepo, updateRequest);
return updateResponse.Commit.Message;
}
catch (NotFoundException)
{
// If the file does not exist, a NotFoundException is thrown. We then create it.
var createRequest = new CreateFileRequest(commitMessage, fileContent, Branch);
var createResponse = await github.Repository.Content.CreateFile(RepoOwner, RepoName, filePathInRepo, createRequest);
return createResponse.Commit.Message;
}
}
///
/// Uploads a binary file (like an image) to the specified path in the repository.
///
///
/// DEPRECATED for larger files. This method can fail silently on files larger than ~1MB. Use UploadFileAsGitBlobAsync instead.
/// This method reads the local file as a byte array and encodes it to Base64, which is required for binary files.
/// It now includes a retry mechanism for file reading to handle potential file locks, plus diagnostic logging and robust error handling.
///
/// The path within the repository where the file will be created.
/// The local file system path of the file to be uploaded.
/// The commit message to associate with the upload operation.
/// A string containing the commit message of the operation, or an error message if it fails.
public static async Task UploadImageFile(string filePathInRepo, string localPath, string commitMessage)
{
try
{
// Get the github client
var github = GetClient();
// --- Start of Diagnostic Checks ---
Console.WriteLine($"\n[DEBUG] Attempting to read file from: {localPath}");
if (!File.Exists(localPath))
{
var errorMsg = $"[ERROR] File not found at path: {localPath}";
Console.WriteLine(errorMsg);
return errorMsg;
}
// --- End of Diagnostic Checks ---
// Read the file content as a byte array with a retry mechanism
byte[] fileBytes = null;
const int maxRetries = 3;
const int delayMilliseconds = 500;
for (int i = 0; i < maxRetries; i++)
{
try
{
fileBytes = await File.ReadAllBytesAsync(localPath);
if (fileBytes.Length > 0)
{
break; // Success, exit loop
}
Console.WriteLine($"[WARN] Attempt {i + 1}: File was read as empty. Retrying after {delayMilliseconds}ms...");
}
catch (IOException ex)
{
Console.WriteLine($"[WARN] Attempt {i + 1}: Could not read file (IO Exception: {ex.Message}). Retrying after {delayMilliseconds}ms...");
}
await Task.Delay(delayMilliseconds);
}
if (fileBytes == null || fileBytes.Length == 0)
{
return "[ERROR] Failed to read file after multiple attempts. It might be locked or empty.";
}
// Convert the byte array to a Base64 string
var fileContentBase64 = Convert.ToBase64String(fileBytes);
// --- Start of Diagnostic Logging ---
Console.WriteLine($"[DEBUG] Original file size: {fileBytes.Length} bytes.");
Console.WriteLine($"[DEBUG] Base64 content length: {fileContentBase64.Length} characters.");
// --- End of Diagnostic Logging ---
// Create a request to create the file in the repository
var createFileRequest = new CreateFileRequest(commitMessage, fileContentBase64, Branch);
// Create the file in the repository
Console.WriteLine("[DEBUG] Sending create file request to GitHub API...");
var createFileResponse = await github.Repository.Content.CreateFile(RepoOwner, RepoName, filePathInRepo, createFileRequest);
Console.WriteLine("[DEBUG] GitHub API request successful.");
return createFileResponse.Commit.Message;
}
catch (ApiException apiEx)
{
// Catch specific GitHub API errors
var errorMsg = $"[ERROR] GitHub API Error: {apiEx.Message}";
Console.WriteLine($"\n{errorMsg}");
Console.WriteLine($"[ERROR] Details: {apiEx.ApiError?.Message}");
return errorMsg;
}
catch (Exception ex)
{
// Catch any other unexpected errors
var errorMsg = $"[ERROR] An unexpected error occurred during file upload: {ex.Message}";
Console.WriteLine($"\n{errorMsg}");
return errorMsg;
}
}
///
/// Uploads a file using the robust Git Data API, suitable for any file size.
///
///
/// This is the recommended method for all file uploads, especially binary files like images.
/// It creates a blob, a new tree, a new commit, and updates the branch reference.
///
public static async Task UploadFileAsGitBlobAsync(string filePathInRepo, string localPath, string commitMessage)
{
// 1. Create the Blob (can stay outside the loop if content is static)
var github = GetClient();
var fileBytes = await File.ReadAllBytesAsync(localPath);
var fileContentAsBase64 = Convert.ToBase64String(fileBytes);
var newBlob = new NewBlob { Content = fileContentAsBase64, Encoding = EncodingType.Base64 };
var blobRef = await github.Git.Blob.Create(RepoOwner, RepoName, newBlob);
Console.WriteLine($"[DEBUG] Created Blob with SHA: {blobRef.Sha}");
int maxRetries = 5;
int retryCount = 0;
bool pushSucceeded = false;
while (!pushSucceeded && retryCount < maxRetries)
{
try
{
// 2. Get the *current* commit and tree
var head = await github.Git.Reference.Get(RepoOwner, RepoName, $"heads/{Branch}");
var latestCommit = await github.Git.Commit.Get(RepoOwner, RepoName, head.Object.Sha);
Console.WriteLine($"[DEBUG] Attempt {retryCount + 1}: Base commit is {latestCommit.Sha}");
// 3. Create a new tree with the new file
var newTree = new NewTree { BaseTree = latestCommit.Tree.Sha };
newTree.Tree.Add(new NewTreeItem { Path = filePathInRepo, Mode = "100644", Type = TreeType.Blob, Sha = blobRef.Sha });
var createdTree = await github.Git.Tree.Create(RepoOwner, RepoName, newTree);
// 4. Create a new commit
var newCommit = new NewCommit(commitMessage, createdTree.Sha, latestCommit.Sha);
var createdCommit = await github.Git.Commit.Create(RepoOwner, RepoName, newCommit);
// 5. Update the branch reference
await github.Git.Reference.Update(RepoOwner, RepoName, $"heads/{Branch}", new ReferenceUpdate(createdCommit.Sha));
Console.WriteLine($"[DEBUG] Branch '{Branch}' updated to new commit: {createdCommit.Sha}");
pushSucceeded = true; // Success! Exit the loop.
return $"Successfully committed {createdCommit.Sha}";
}
catch (ApiValidationException ex) when (ex.Message.Contains("Update is not a fast forward"))
{
// Race condition detected. Increment retry count and loop again.
retryCount++;
Console.WriteLine($"[WARN] Race condition detected. Retrying attempt {retryCount}...");
await Task.Delay(100 * retryCount); // Optional: add a small, increasing delay
}
// Note: Other exceptions will fall through and fail, as they should.
}
//// If we exit the loop without success
//throw new Exception($"Failed to commit to branch '{Branch}' after {maxRetries} attempts due to repeated race conditions.");
//var github = GetClient();
//// 1. Create the Blob
//var fileBytes = await File.ReadAllBytesAsync(localPath);
//var fileContentBase64 = Convert.ToBase64String(fileBytes);
//var newBlob = new NewBlob { Content = fileContentBase64, Encoding = EncodingType.Base64 };
//var blobRef = await github.Git.Blob.Create(RepoOwner, RepoName, newBlob);
//Console.WriteLine($"[DEBUG] Created blob with SHA: {blobRef.Sha}");
//// 2. Get the current commit and tree
//var head = await github.Git.Reference.Get(RepoOwner, RepoName, $"heads/{Branch}");
//var latestCommit = await github.Git.Commit.Get(RepoOwner, RepoName, head.Object.Sha);
//// 3. Create a new tree with the new file
//var newTree = new NewTree { BaseTree = latestCommit.Tree.Sha };
//newTree.Tree.Add(new NewTreeItem { Path = filePathInRepo, Mode = "100644", Type = TreeType.Blob, Sha = blobRef.Sha });
//var createdTree = await github.Git.Tree.Create(RepoOwner, RepoName, newTree);
//Console.WriteLine($"[DEBUG] Created tree with SHA: {createdTree.Sha}");
//// 4. Create a new commit
//var newCommit = new NewCommit(commitMessage, createdTree.Sha, latestCommit.Sha);
//var createdCommit = await github.Git.Commit.Create(RepoOwner, RepoName, newCommit);
//Console.WriteLine($"[DEBUG] Created commit with SHA: {createdCommit.Sha}");
//// 5. Update the branch reference to point to the new commit
//await github.Git.Reference.Update(RepoOwner, RepoName, $"heads/{Branch}", new ReferenceUpdate(createdCommit.Sha));
//Console.WriteLine($"[DEBUG] Branch '{Branch}' updated to new commit.");
return "Failed";
}
// Helper method to check if a file exists and get its SHA
private static async Task GetFileSha(IGitHubClient github, string filePath)
{
try
{
// Get all content in the file's directory
var contents = await github.Repository.Content.GetAllContents(RepoOwner, RepoName, filePath);
// Return the specific file's metadata
return contents.FirstOrDefault();
}
catch (NotFoundException)
{
// If the file or directory doesn't exist, a NotFoundException is thrown
return null;
}
}
}
}