5 Commits

20 changed files with 805 additions and 311 deletions

22
git-anchor.sln Normal file
View File

@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "git-anchor", "git-anchor\git-anchor.csproj", "{9B181097-6E9E-4FA1-8167-BFE34C03D08D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9B181097-6E9E-4FA1-8167-BFE34C03D08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B181097-6E9E-4FA1-8167-BFE34C03D08D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B181097-6E9E-4FA1-8167-BFE34C03D08D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B181097-6E9E-4FA1-8167-BFE34C03D08D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,15 @@
using System.Diagnostics;
namespace GitAnchor.Actions;
public abstract class BaseAction {
public virtual void Run()
{
throw new NotImplementedException();
}
public virtual async Task<ActivityStatusCode> RunAsync()
{
await Task.Delay(0);
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,99 @@
using GitAnchor.Lib;
using Octokit;
using System.Diagnostics;
namespace GitAnchor.Actions;
public class Create : BaseAction
{
public new static async Task<ActivityStatusCode> RunAsync()
{
string[] args = Environment.GetCommandLineArgs();
string? token = args.FirstOrDefault(arg => arg.StartsWith("--token"));
string? backupName = args.FirstOrDefault(
arg => arg.StartsWith("--name") || arg.StartsWith("-n"))?
.Split("=")[1].Trim();
bool verbose = args.Any(arg => arg.StartsWith("--verbose")
|| arg.StartsWith("-v"));
// find or configure the default backup directory
DirectoryInfo backupDir = FileSystemTools.SetUpMainBackupFile()
?? throw new Exception("Encountered a problem creating main backup directory");
// create backup file
DirectoryInfo? anchorDir = FileSystemTools.SetUpAnchor(backupDir, backupName);
if (anchorDir == null)
{
Console.WriteLine("Could not create anchor directory.");
return ActivityStatusCode.Error;
}
// authenticate with github
GitHubClient github = GitHub.CreateClient();
token = GitHub.Authenticate(github, token);
// load accessible repositories
IEnumerable<Repository> repos = await GitHub.GetRepos(github) ?? throw new Exception("No repos found");
// organize and ensure all repositories are unique
HashSet<Repository> uniqueRepos = GitHub.GetUniqueRepos(repos);
if (verbose)
{
// derive repository count and clone urls
var privateRepos = uniqueRepos.Where(r => r.Private);
var publicRepos = uniqueRepos.Where(r => !r.Private);
var cloneUrls = uniqueRepos.Select(r => r.CloneUrl);
Console.WriteLine($"Found {privateRepos.Count()} private repos and {publicRepos.Count()} public repos.");
}
// get user confirmation to proceed
Console.WriteLine($"\n\nProceed to clone {repos.Count()} repositories? (y/n)");
string? response = Console.ReadLine();
if (response?.ToLower() != "y")
{
Console.WriteLine("Aborting...");
return ActivityStatusCode.Ok;
}
// set up, update directories for hosting projects
HashSet<Repository> newProjects = Volumes.PopulateBackupDirectories(uniqueRepos, anchorDir);
// identify all work to be done and prepare as tasks
List<Task> taskPool = [];
foreach (Repository newProject in newProjects)
{
GitHub.GithubRepoOptions options = new()
{
BackupDir = anchorDir,
Repo = newProject,
Token = token,
User = github.User.Current().Result,
};
taskPool.Add(new Task(() => GitHub.CloneProject(options)));
}
Console.WriteLine($"Preparing to clone {taskPool.Count} repositories. Starting...");
// start a timer to track progress
using var reporter = new ProgressReporter();
reporter.Start();
Parallel.ForEach(taskPool, task => task.Start());
Task.WaitAll([.. taskPool]);
reporter.Stop();
var report = reporter.ToString();
Console.WriteLine(report);
return ActivityStatusCode.Ok;
}
}

View File

@@ -0,0 +1,37 @@
using System.Diagnostics;
namespace GitAnchor.Actions;
public class Help : BaseAction
{
public static new ActivityStatusCode Run()
{
Console.WriteLine("\nGitAnchor - a tool for performing scoped backups of GitHub repositories based on credentials and configuration.");
Console.WriteLine("Usage:\n");
Console.WriteLine("git-anchor create [--verbose, -v] [--name <name>] [--token <token>]");
Console.WriteLine(" Creates a new backup directory and clones all repositories accessible by the provided token.");
Console.WriteLine(" --name <name> The name of the backup directory to create. Defaults to the current date.");
Console.WriteLine(" --token <token> The GitHub token to use for authentication.");
Console.WriteLine(" --verbose Prints verbose output.\n");
Console.WriteLine("Example: git-anchor create -v --name=cool-new-backup --token=(your token)");
Console.WriteLine("git-anchor pull [--name <name>] [--verbose, -v]");
Console.WriteLine(" Pulls all repositories in an existing backup directory.");
Console.WriteLine(" --name <name> The name of the backup directory to pull from. Defaults to the current date.");
Console.WriteLine(" --verbose Prints verbose output.\n");
Console.WriteLine("Example: git-anchor pull -v --name=cool-new-backup");
Console.WriteLine("git-anchor list");
Console.WriteLine(" Lists all existing backups.\n");
Console.WriteLine("Example: git-anchor list");
Console.WriteLine("git-anchor find [--regex, -e] [--name, -n <name>]");
Console.WriteLine(" Finds an existing backup matching the provided pattern.");
Console.WriteLine(" --regex, -e Find your backup directory based on a Regex search.");
Console.WriteLine(" --name, -n The name of the backup directory to pull from. Defaults to the current date.\n\n");
return ActivityStatusCode.Ok;
}
}

View File

@@ -0,0 +1,35 @@
using System.Diagnostics;
using GitAnchor.Lib;
namespace GitAnchor.Actions;
class List : BaseAction
{
public static new ActivityStatusCode Run()
{
// get main backup directory
var backupLocation = FileSystemTools.MainBackupFile;
var backupDir = new DirectoryInfo(backupLocation ?? throw new Exception("No main backup found"));
string first = "";
// get all directories in main backup directory
var directories = backupDir.GetDirectories();
Console.WriteLine("Found the following backups:\n");
// print all directories
foreach (var dir in directories)
{
if (first == "") first = dir.Name;
long size = FileSystemTools.GetBackupSize(dir);
long sizeInGBs = Volumes.GetSizeInGB(size);
long sizeInMBs = Volumes.GetSizeInMB(size);
Console.WriteLine($"{dir.Name} - {(sizeInGBs > 0 ? sizeInGBs : sizeInMBs)}{(sizeInGBs > 0 ? "GB" : "MB")}");
}
Console.WriteLine("\nUse git-anchor pull --name=<name> to pull a backup.");
Console.WriteLine($"Example: git-anchor pull --name={first} -v\n");
return ActivityStatusCode.Ok;
}
}

View File

@@ -0,0 +1,80 @@
using System.Diagnostics;
using GitAnchor.Lib;
using GitAnchor.Actions;
using Octokit;
namespace GitAnchor.Actions
{
internal class Pull : BaseAction
{
public new static async Task<ActivityStatusCode> RunAsync()
{
string[] args = Environment.GetCommandLineArgs();
// get main backup
var backupLocation = FileSystemTools.MainBackupFile;
var mainBackupDir = new DirectoryInfo(backupLocation ?? throw new Exception("No main backup found"));
// act on provided arguments
string? token = args.FirstOrDefault(arg => arg.StartsWith("--token"));
bool verbose = args.Any(arg => arg.StartsWith("--verbose") || arg.StartsWith("-v"));
string anchorName = args.FirstOrDefault(arg => arg.StartsWith("--name") || arg.StartsWith("-n"))?.Split("=")[1].Trim()
?? throw new Exception("No anchor name provided");
// find (or create) the anchor in question
var anchor = Volumes.GetAnchor(mainBackupDir, anchorName) ?? FileSystemTools.SetUpAnchor(mainBackupDir, anchorName);
var projectDirectories = anchor?.GetDirectories();
if (projectDirectories == null)
{
Console.WriteLine("No projects found in anchor.");
return ActivityStatusCode.Error;
}
// set up our github connection
GitHubClient github = GitHub.CreateClient();
token = GitHub.Authenticate(github, token);
var repoList = await GitHub.GetRepos(github);
// set up a task pool for all pull jobs
List<Task> taskPool = [];
// iterate through all projects and prepare jobs
foreach (DirectoryInfo backupDir in projectDirectories)
{
var repo = repoList?.Where(r => r.Name == backupDir.Name).FirstOrDefault();
if (repo == null) continue;
if (verbose) Console.WriteLine($"Preparing pull task for {repo.Name}");
var options = new GitHub.GithubRepoOptions
{
Repo = repo,
BackupDir = backupDir,
Verbose = verbose,
};
Task pullTask = new(() => GitHub.PullExistingRepo(options));
taskPool.Add(pullTask);
}
// start progress tracker
using var reporter = new ProgressReporter();
reporter.Start();
Parallel.ForEach(taskPool, task => task.Start());
Task.WaitAll([.. taskPool]);
reporter.Stop();
if (verbose)
{
var report = reporter.ToString();
Console.WriteLine(report);
}
return ActivityStatusCode.Ok;
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Diagnostics;
using GitAnchor.Lib;
namespace GitAnchor.Actions;
public class Status : BaseAction
{
public new static async Task<ActivityStatusCode> RunAsync()
{
string[] args = Environment.GetCommandLineArgs();
// get main backup directory
var backupLocation = FileSystemTools.MainBackupFile;
var anchorName = CommandLineTools.ReadAnchorName();
var backupDir = new DirectoryInfo(Path.Join(backupLocation, anchorName));
// get all directories in main backup directory
var directories = backupDir.GetDirectories();
List<Task> taskPool = [];
foreach (var dir in directories)
{
GitHub.GithubRepoOptions options = new()
{
AnchorPath = Path.Join(backupDir.FullName, dir.Name),
BackupDir = backupDir,
ProjectPath = dir.Name,
};
taskPool.Add(Task.Run(() => GitHub.QueryStatus(options)));
}
using var reporter = new ProgressReporter();
reporter.Start();
await Parallel.ForEachAsync(taskPool, async(task, cancellationToken) => await task);
reporter.Stop();
Console.WriteLine(reporter.ToString());
return ActivityStatusCode.Ok;
}
}

View File

@@ -0,0 +1,10 @@
using System.Diagnostics;
namespace GitAnchor.Lib;
public partial class ActionResult
{
}
delegate Task<T> Callable<T>();

View File

@@ -0,0 +1,45 @@
using Octokit;
namespace GitAnchor.Lib;
public static class CommandLineTools {
public static string? ReadAnchorName()
{
return Environment.GetCommandLineArgs().FirstOrDefault(arg => arg.StartsWith("--name") || arg.StartsWith("-n"))?
.Split("=")[1].Trim();
}
public static int ReadAsInt()
{
string? input = Console.ReadLine();
while (input == null)
{
input = Console.ReadLine();
}
try
{
return int.Parse(input);
}
catch
{
Console.WriteLine("Invaid input. Please enter a number: ");
return ReadAsInt();
}
}
public static string ReadAccessToken(GitHubClient github)
{
Console.WriteLine("Please enter your Github Personal Access Token: ");
string? token = Console.ReadLine();
if (token == null)
{
Console.WriteLine("Received invalid input. Try again:");
return ReadAccessToken(github);
}
return token;
}
}

View File

@@ -0,0 +1,152 @@
using System.Runtime.InteropServices;
namespace GitAnchor.Lib;
public static class FileSystemTools {
public static string DefaultAnchorPath
{
get => DateTime.Now.ToString("Mddy");
}
public static bool MainBackupExists
{
get => MainBackupFile != null;
}
public static bool AnchorExists(string anchorName)
{
return MainBackupExists && Directory.Exists($"{MainBackupFile}/anchorName");
}
public static string? MainBackupFile
{
get => Volumes.FindMainBackupFile();
}
public static long GetBackupSize(DirectoryInfo dir)
{
long size = 0;
foreach (FileInfo fi in dir.GetFiles("*", SearchOption.AllDirectories))
{
size += fi.Length;
}
return size;
}
public static DirectoryInfo? SetUpMainBackupFile()
{
IEnumerable<DriveInfo> volumes = Volumes.GetVolumes();
DriveInfo? selection = Volumes.SelectFromList(volumes);
if (selection == null)
{
Console.WriteLine("No selection found");
return null;
}
if (MainBackupFile != null) return new DirectoryInfo(MainBackupFile);
bool isWindows = OperatingSystem.IsWindows();
bool isMacOS = OperatingSystem.IsMacOS();
bool isLinux = OperatingSystem.IsLinux();
if (isWindows)
return CreateMainWindowsDirectory(selection);
else if (isLinux || isMacOS)
return CreateMainUnixDirectory(selection);
else
throw new Exception("Unsupported operating system");
}
public static DirectoryInfo? SetUpAnchor(DirectoryInfo backupDir, string? initialInput)
{
string defaultName = FileSystemTools.DefaultAnchorPath;
string? backupName = initialInput;
if (backupName == null)
{
Console.WriteLine($"Enter a name for this backup: (default {defaultName})");
backupName = Console.ReadLine();
if (backupName == "") backupName = defaultName;
}
if (backupName == null || backupName == "") throw new Exception("No backup name provided");
// create backup file
return Volumes.CreateAnchor(backupDir, backupName);
}
private static DirectoryInfo? CreateMainWindowsDirectory(DriveInfo volume, string? location = ".git-anchor")
{
OSPlatform platform;
if (OperatingSystem.IsWindows())
{
platform = OSPlatform.Windows;
}
else if (OperatingSystem.IsMacOS())
{
platform = OSPlatform.OSX;
}
else if (OperatingSystem.IsLinux())
{
platform = OSPlatform.Linux;
}
else
{
throw new Exception("Unsupported operating system");
}
bool isSystemDrive;
bool isExternalDrive;
if (platform == OSPlatform.Windows)
{
isSystemDrive = volume.Name == "C:\\";
isExternalDrive = volume.DriveType == DriveType.Removable && !isSystemDrive;
}
else
{
isSystemDrive = volume.Name == "/";
isExternalDrive = volume.DriveType == DriveType.Removable && !isSystemDrive;
}
try
{
if (isSystemDrive)
{
Console.WriteLine("Using system drive. Directing you to user folder...");
string? userPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
DirectoryInfo userDir = new(userPath);
Console.WriteLine($"Checking for folder `.ghbackups` at `{userDir.FullName}`...");
DirectoryInfo? backupDir = userDir.CreateSubdirectory(".ghbackups");
return backupDir;
}
else
{
Console.WriteLine($"Using {volume.Name}. Directing you to root folder...");
string rootPath = volume.RootDirectory.FullName;
DirectoryInfo rootDir = new(rootPath);
Console.WriteLine($"Checking for folder `.ghbackups` at `{rootDir.FullName}`...");
DirectoryInfo backupDir = Directory.CreateDirectory($"{rootDir.FullName}.ghbackups");
return backupDir;
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return null;
}
}
private static DirectoryInfo? CreateMainUnixDirectory(DriveInfo volume)
{
throw new NotImplementedException();
}
}

View File

@@ -1,30 +1,28 @@
using Octokit;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace RepoBackupUtil.Lib
namespace GitAnchor.Lib
{
public class GitHub
{
public static string GetCredentialsToken(GitHubClient github)
public static GitHubClient CreateClient()
{
Console.WriteLine("Please enter your Github Personal Access Token: ");
string? token = Console.ReadLine();
if (token == null)
{
Console.WriteLine("Received invalid input. Try again:");
return GetCredentialsToken(github);
ProductHeaderValue productHeaderValue = new("git-anchor");
return new(productHeaderValue);
}
else
public static string Authenticate(GitHubClient github, string? initialToken = null)
{
string token = initialToken ?? CommandLineTools.ReadAccessToken(github);
AuthenticationType authType = AuthenticationType.Bearer;
Credentials auth = new(token, authType);
github.Credentials = auth;
Console.WriteLine("Successfully authenticated with GitHub.");
return token;
}
}
/** @deprecated */
public static string SetOAuthCredentials(GitHubClient github)
{
Console.WriteLine("Provide OAuth Client Secret: ");
@@ -83,21 +81,27 @@ namespace RepoBackupUtil.Lib
return projectsToUpdate;
}
public record CloneProjectOptions : ICloneProject
public record GithubRepoOptions
{
public User User { get; init; } = default!;
public Repository Repo { get; init; } = default!;
public DirectoryInfo BackupDir { get; init; } = default!;
public string Token { get; init; } = default!;
public User? User { get; init; } = default!;
public Repository? Repo { get; init; } = default!;
public string? ProjectPath { get; init; }
public string? Token { get; init; }
public string? AnchorPath { get; init; }
public bool? Verbose { get; init; } = false;
}
public static void CloneProject(ICloneProject options)
public static void CloneProject(GithubRepoOptions options)
{
if (options.User == null) throw new Exception("User not found");
if (options.Repo == null) throw new Exception("Repository not found");
string cloneString = "";
cloneString += $"clone https://{options.Token}@github.com/";
cloneString += $"{options.User.Login}/{options.Repo.Name} .";
Console.WriteLine($"Cloning {options.Repo.Name}...");
if (options.Verbose ?? false) Console.WriteLine($"Cloning {options.Repo.Name}...");
using (Process process = new())
{
@@ -105,20 +109,22 @@ namespace RepoBackupUtil.Lib
{
FileName = "git",
Arguments = cloneString,
WorkingDirectory = $"{options.BackupDir}/{options.Repo.Name}",
WorkingDirectory = Path.Join(options.BackupDir.FullName, options.Repo.Name),
RedirectStandardOutput = true,
RedirectStandardError = true
};
process.Start();
Console.WriteLine(process.StandardOutput.ReadToEnd());
if (options.Verbose ?? false) Console.WriteLine(process.StandardOutput.ReadToEnd());
process.WaitForExit();
Console.WriteLine($"Clone complete for {options.Repo.Name}");
};
}
public static void UpdateExistingRepo(Repository repo, DirectoryInfo backupDir)
public static void PullExistingRepo(GithubRepoOptions options)
{
Console.WriteLine($"Preparing update task for {repo.Name}...");
if (options.Repo == null) throw new Exception("Repository not found");
Console.WriteLine($"Pulling {options.Repo.Name}...");
using (Process process = new())
{
@@ -126,7 +132,27 @@ namespace RepoBackupUtil.Lib
{
FileName = "git",
Arguments = "pull",
WorkingDirectory = $"{backupDir}/{repo.Name}",
WorkingDirectory = Path.Join(options.BackupDir.FullName, options.Repo.Name),
RedirectStandardOutput = true,
RedirectStandardError = true
};
process.Start();
if (options.Verbose ?? false) Console.WriteLine(process.StandardOutput.ReadToEnd());
process.WaitForExit();
Console.WriteLine($"Pull complete for {options.Repo.Name}");
};
}
public static void QueryStatus(GithubRepoOptions options)
{
using (Process process = new())
{
process.StartInfo = new()
{
FileName = "git",
Arguments = "status",
WorkingDirectory = Path.Join(options.BackupDir.FullName, options.Repo?.Name ?? options.ProjectPath ?? throw new Exception("Unable to determine working directory")),
RedirectStandardOutput = true,
RedirectStandardError = true
};

View File

@@ -0,0 +1,31 @@
using System.Diagnostics;
namespace GitAnchor.Lib;
public class ProgressReporter : Stopwatch, IDisposable
{
public ProgressReporter()
{
// progress = new();
}
public void Dispose()
{
this.Stop();
// progress = null!;
GC.SuppressFinalize(this);
}
public override string ToString()
{
// progress.ProgressChanged += (sender, message) => Console.WriteLine(message);
var elapsed = this.ElapsedMilliseconds < 1000
? this.ElapsedMilliseconds
: this.ElapsedMilliseconds / 1000;
string unit = this.ElapsedMilliseconds < 1000 ? "ms" : "s";
return $"All tasks completed in {elapsed}{unit}. Exiting...";
}
}

5
git-anchor/Lib/Union.cs Normal file
View File

@@ -0,0 +1,5 @@
namespace GitAnchor.Lib;
public class Union<T1, T2>
{
}

124
git-anchor/Lib/Volumes.cs Normal file
View File

@@ -0,0 +1,124 @@
using Octokit;
using System.Collections;
namespace GitAnchor.Lib;
public static class Volumes
{
public static DriveInfo[] GetVolumes()
{
try
{
return DriveInfo.GetDrives();
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine("Unauthorized access to volumes. Please run as administrator.");
Console.WriteLine(e.Message);
return [];
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return [];
}
}
private static DriveInfo? GetSelectedDrive(Hashtable options)
{
int inputAsInt = CommandLineTools.ReadAsInt();
if (!options.ContainsKey(inputAsInt)) return null;
foreach (DictionaryEntry entry in options)
{
if (entry.Key.Equals(inputAsInt))
{
return entry.Value != null ? (DriveInfo)entry.Value : null;
}
}
return null;
}
public static DriveInfo? SelectFromList(IEnumerable<DriveInfo> volumes)
{
int i = 0;
Hashtable options = [];
// prepare table and present options to user
Console.WriteLine("Select the drive you want to backup to (enter a number): \n");
foreach (DriveInfo volume in volumes)
{
++i;
string option = $"{i}: {volume.Name} ({GetSizeInGB(volume)}GB available)";
Console.WriteLine(option);
options.Add(i, volume);
}
try
{
// parse user input and return appropiate drive info
return GetSelectedDrive(options);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return null;
}
}
public static DirectoryInfo? CreateAnchor(DirectoryInfo backupDir, string anchorName)
{
return backupDir.CreateSubdirectory(anchorName);
}
public static DirectoryInfo? GetAnchor(DirectoryInfo backupDir, string anchorName)
{
return backupDir.GetDirectories().FirstOrDefault(dir => dir.Name == anchorName);
}
public static string? FindMainBackupFile()
{
// find a folder entitled `.ghbackups`
// check the system drive first, then scan for additional drives and check the root of each
var userFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
bool backupExistsInUserFolder = Directory.Exists($"{userFolder}/.ghbackups");
if (backupExistsInUserFolder) return $"{userFolder}/.ghbackups";
DriveInfo[] volumes = GetVolumes();
foreach (DriveInfo volume in volumes)
{
bool backupExistsInRoot = Directory.Exists($"{volume.RootDirectory.FullName}/.ghbackups");
if (backupExistsInRoot) return $"{volume.RootDirectory.FullName}/.ghbackups";
}
return null;
}
public static long GetSizeInMB(long bytes) => bytes / 1024 / 1024;
public static long GetSizeInGB(long bytes) => bytes / 1024 / 1024 / 1024;
public static long GetSizeInGB(DriveInfo volume) => volume.TotalSize / 1024 / 1024 / 1024;
public static HashSet<Repository> PopulateBackupDirectories(HashSet<Repository> repos, DirectoryInfo backupDir)
{
HashSet<Repository> newProjects = [];
foreach (Repository repo in repos)
{
bool exists = Directory.Exists($"{backupDir.FullName}/{repo.Name}");
bool hasContents = exists && Directory.EnumerateFileSystemEntries(
$"{backupDir.FullName}/{repo.Name}").Any();
if (exists && hasContents) continue;
DirectoryInfo? repoDir = backupDir.CreateSubdirectory(repo.Name);
Console.WriteLine($"Created directory for project {repoDir.FullName}");
newProjects.Add(repo);
}
return newProjects;
}
}

49
git-anchor/Program.cs Normal file
View File

@@ -0,0 +1,49 @@
using System.Collections;
using System.Diagnostics;
using System.Reflection;
using GitAnchor.Actions;
string[] inputArgs = Environment.GetCommandLineArgs();
// create pointers for entrypoints for each action
var createFn = typeof(Create).GetMethod("RunAsync", BindingFlags.Static | BindingFlags.Public);
var pullFn = typeof(Pull).GetMethod("RunAsync", BindingFlags.Static | BindingFlags.Public);
var statusFn = typeof(Status).GetMethod("RunAsync", BindingFlags.Static | BindingFlags.Public);
var listFn = typeof(List).GetMethod("Run", BindingFlags.Static | BindingFlags.Public);
var helpFn = typeof(Help).GetMethod("Run", BindingFlags.Static | BindingFlags.Public);
var AllOptions = new Hashtable()
{
{ "create", createFn },
{ "pull", pullFn },
{ "status", statusFn },
{ "list", listFn },
{ "help", helpFn },
};
try
{
foreach (string a in inputArgs)
{
var titleCase = a.ToUpperInvariant();
if (AllOptions.ContainsKey(a))
{
var func = (MethodInfo)AllOptions[a];
var result = func?.Invoke(null, null);
if (result is Task<ActivityStatusCode> task)
{
return (int)task.Result;
}
};
}
return (int)Help.Run();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return (int)ActivityStatusCode.Error;
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>RepoBackupUtil</RootNamespace>
<RootNamespace>GitAnchor</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@@ -1,25 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34309.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "repo-backup-util", "repo-backup-util\repo-backup-util.csproj", "{352652F3-958B-416B-B5F5-CDC36B0AA0C4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{352652F3-958B-416B-B5F5-CDC36B0AA0C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{352652F3-958B-416B-B5F5-CDC36B0AA0C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{352652F3-958B-416B-B5F5-CDC36B0AA0C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{352652F3-958B-416B-B5F5-CDC36B0AA0C4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46CA2A1B-A03D-4D97-9DDF-4BCABF53C54A}
EndGlobalSection
EndGlobal

View File

@@ -1,82 +0,0 @@
// See https://aka.ms/new-console-template for more information
using Octokit;
using RepoBackupUtil.Lib;
using System.Diagnostics;
static async Task Main()
{
// initialize our github client
ProductHeaderValue productHeaderValue = new("repo-backup-util");
GitHubClient github = new(productHeaderValue);
// check for user with received input
string token = GitHub.GetCredentialsToken(github);
string username = github.User.Current().Result.Login;
User user = await GitHub.GetUser(username, github);
// get all repos for user
IEnumerable<Repository> repos = await GitHub.GetRepos(github) ?? throw new Exception("No repos found");
// organize and ensure all repositories are unique
HashSet<Repository> uniqueRepos = GitHub.GetUniqueRepos(repos);
// derive repository count and clone urls
var privateRepos = uniqueRepos.Where(r => r.Private);
var publicRepos = uniqueRepos.Where(r => !r.Private);
var cloneUrls = uniqueRepos.Select(r => r.CloneUrl);
Console.WriteLine($"Found {privateRepos.Count()} private repos and {publicRepos.Count()} public repos.");
// create backup directory
DirectoryInfo backupDir = Volumes.SetUpBackupFile() ?? throw new Exception("Error setting up backup directory");
// set up, update directories for hosting projects
HashSet<Repository> newProjects = Volumes.PopulateBackupDirectories(uniqueRepos, backupDir);
IEnumerable<Repository> projectsToUpdate = GitHub.GetProjectsToUpdate(uniqueRepos, backupDir);
Console.WriteLine(projectsToUpdate.Count() + " projects to update.");
Console.WriteLine(newProjects.Count + " new projects to clone.");
// identify all work to be done and prepare as tasks
List<Task> taskPool = [];
foreach (Repository project in projectsToUpdate)
{
taskPool.Add(new Task(() => GitHub.UpdateExistingRepo(project, backupDir)));
}
foreach (Repository newProject in newProjects)
{
ICloneProject options = new GitHub.CloneProjectOptions
{
BackupDir = backupDir,
Repo = newProject,
Token = token,
User = user
};
taskPool.Add(new Task(() => GitHub.CloneProject(options)));
}
Console.WriteLine($"Prepared {taskPool.Count} tasks. Starting...");
// start a timer to track progress
Stopwatch stopwatch = new();
stopwatch.Start();
// execute all tasks
Parallel.ForEach(taskPool, task => task.Start());
Task.WaitAll(taskPool.ToArray());
stopwatch.Stop();
double elapsed = stopwatch.ElapsedMilliseconds < 1000
? stopwatch.ElapsedMilliseconds
: stopwatch.ElapsedMilliseconds / 1000;
string unit = stopwatch.ElapsedMilliseconds < 1000 ? "ms" : "s";
Console.WriteLine($"All tasks completed in {elapsed}{unit}. Exiting...");
}
await Main();

View File

@@ -1,10 +0,0 @@
using Octokit;
namespace RepoBackupUtil.Lib;
public interface ICloneProject
{
Repository Repo { get; }
DirectoryInfo BackupDir { get; }
User User { get; }
string Token { get; }
}

View File

@@ -1,163 +0,0 @@
using Octokit;
using System.Collections;
namespace RepoBackupUtil.Lib
{
public static class Volumes
{
public static DirectoryInfo? SetUpBackupFile()
{
IEnumerable<DriveInfo> volumes = GetVolumes();
DriveInfo? selection = SelectFromList(volumes);
if (selection == null)
{
Console.WriteLine("No selection found");
return null;
}
var backupDir = CreateBackupDirectory(selection);
return backupDir;
}
public static IEnumerable<DriveInfo> GetVolumes()
{
try
{
return DriveInfo.GetDrives();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return [];
}
}
private static int ReadInput()
{
string? input = Console.ReadLine();
while (input == null)
{
Console.WriteLine("Please enter a number: ");
input = Console.ReadLine();
}
try
{
return int.Parse(input);
}
catch
{
Console.WriteLine("Invaid input. Please enter a number: ");
return ReadInput();
}
}
private static DriveInfo? GetSelection(Hashtable options)
{
int inputAsInt = ReadInput();
if (!options.ContainsKey(inputAsInt))
{
return null;
}
foreach (DictionaryEntry entry in options)
{
if (entry.Key.Equals(inputAsInt))
{
Console.WriteLine($"value: {entry.Value}");
Console.WriteLine($"type: {entry.Value?.GetType()}");
return entry.Value != null ? (DriveInfo)entry.Value : null;
}
}
return null;
}
public static DriveInfo? SelectFromList(IEnumerable<DriveInfo> volumes)
{
int i = 0;
Hashtable options = [];
// prepare table and present options to user
Console.WriteLine("Select the drive you want to backup to (enter a number): \n");
foreach (DriveInfo volume in volumes)
{
++i;
string option = $"{i}: {volume.Name} ({GetSizeInGB(volume)}GB available)";
Console.WriteLine(option);
options.Add(i, volume);
}
try
{
// parse user input and return appropiate drive info
return GetSelection(options);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return null;
}
}
public static double GetSizeInGB(DriveInfo volume) => volume.TotalSize / 1024 / 1024 / 1024;
public static DirectoryInfo? CreateBackupDirectory(DriveInfo volume)
{
bool isSystemDrive = volume.DriveType == DriveType.Fixed;
try
{
if (isSystemDrive)
{
Console.WriteLine("Using system drive. Directing you to user folder...");
string? userPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
DirectoryInfo userDir = new(userPath);
Console.WriteLine($"Checking for folder `.ghbackups` at `{userDir.FullName}`...");
DirectoryInfo? backupDir = userDir.CreateSubdirectory(".ghbackups");
return backupDir;
}
else
{
Console.WriteLine($"Using {volume.Name}. Directing you to root folder...");
DirectoryInfo? rootDir = volume.RootDirectory;
Console.WriteLine($"Checking for folder `.ghbackups` at `{rootDir.FullName}`...");
DirectoryInfo? backupDir = rootDir.CreateSubdirectory(".ghbackups");
return backupDir;
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return null;
}
}
public static HashSet<Repository> PopulateBackupDirectories(HashSet<Repository> repos, DirectoryInfo backupDir)
{
HashSet<Repository> newProjects = [];
foreach (Repository repo in repos)
{
bool exists = Directory.Exists($"{backupDir.FullName}/{repo.Name}");
bool hasContents = exists && Directory.EnumerateFileSystemEntries(
$"{backupDir.FullName}/{repo.Name}").Any();
if (exists && hasContents) continue;
DirectoryInfo? repoDir = backupDir.CreateSubdirectory(repo.Name);
Console.WriteLine($"Created directory for project {repoDir.FullName}");
newProjects.Add(repo);
}
return newProjects;
}
}
}