Skip to content

Commit

Permalink
Add a new relative target path heuristic with / in url
Browse files Browse the repository at this point in the history
When the source url ends in `/`, work similarly to when the target file path ends with `/`, meaning "relative structure from here on". This allows mapping an abitrary subfolder in GitHub to a specific target base directory on the local machine.
  • Loading branch information
kzu committed Jul 8, 2024
1 parent 5f60c8c commit cc23040
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 17 deletions.
9 changes: 5 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ Examples:
dotnet file changes // lists all configured files and their status with regards to the configured
// remote URL and ETag matching

> NOTE: to download a file from GitHub to the current directory, ignoring the remote folder structure,
> specify `.` as the `[file]` argument after the `[url]`. Otherwise, the default will be to match the
> directory structure of the source file.
The target path is determined by the following rules:
* If `[file]` = `.`: download to the current directory, ignoring the source directory structure.
* If `[url]` ends with `/`: download to the current directory, preserving the source directory structure
from that point onwards (i.e. for GitHub tree/dir URLs)
* Otherwise, match the directory structure of the source file.

After downloading a file, a new entry is created in a local `.netconfig` file, which
leverages [dotnet config](https://github.com/kzu/dotnet-config):
Expand Down
2 changes: 1 addition & 1 deletion src/File/File.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<PackageReference Include="ColoredConsole" Version="1.0.0" />
<PackageReference Include="Spectre.Console" Version="0.48.0" />
<PackageReference Include="Mono.Options" Version="6.12.0.148" />
<PackageReference Include="DotNetConfig" Version="1.1.1" />
<PackageReference Include="DotNetConfig" Version="1.2.0" />
<PackageReference Include="git-credential-manager" Version="2.4.1" IncludeAssets="tools" GeneratePathProperty="true" />
</ItemGroup>

Expand Down
15 changes: 11 additions & 4 deletions src/File/FileSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,17 @@ static FileSpec WithGitHubUri(string baseDir, Uri uri, Func<Uri, FileSpec> next)
// This is a whole directory URL, so use that as the base dir,
// denoted by the ending in a path separator. Note we skip 4 parts
// since those are org/repo/tree/branch, then comes the actual dir.
return new FileSpec(
flatten ? baseDir :
baseDir.Length == 0 ? string.Join('/', parts.Skip(4)) :
System.IO.Path.Combine(baseDir, string.Join('/', parts.Skip(4))).Replace('\\', '/') + "/",
return new FileSpec(flatten
? baseDir // Flattened folder structure, just use base dir as target path
: baseDir.Length == 0 // Otherwise, perform some heuristics to determine the target path
? uri.PathAndQuery.EndsWith('/')
// A URI ending in / has a similar effect to a target path ending in /, meaning
// Copy the whole directory structure to the target directory from that path onwards,
// without prepending the source (web) directory structure.
? ""
// Otherwise, just like previous behavior, prepend upstream dir structure.
: string.Join('/', parts.Skip(4))
: System.IO.Path.Combine(baseDir, string.Join('/', parts.Skip(4))).Replace('\\', '/') + "/",
uri, finalPath: true);
}
else if (parts[2] == "blob" || parts[2] == "raw")
Expand Down
26 changes: 18 additions & 8 deletions src/File/GitHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static GitHubResult TryGetFiles(FileSpec spec, out List<FileSpec> result)
{
if (parts[2] == "tree")
{
// tree urls contain branch and optionally
// tree urls contain branch and optionally a subdirectory
branch = parts[3];
if (parts.Length >= 4)
repoDir = string.Join('/', parts[4..]);
Expand All @@ -85,19 +85,24 @@ public static GitHubResult TryGetFiles(FileSpec spec, out List<FileSpec> result)

Console.Write("=> fetching via gh cli");

// Construct target path by combining the baseDir and only the relative dir
// from each file's URL except for the source path.
var relativeFrom = spec.Uri.PathAndQuery.EndsWith('/') ? (repoDir ?? "") : "";

if (Process.TryExecute("gh", "api " + apiUrl + apiPath + apiQuery, out var data) &&
JsonConvert.DeserializeObject<JToken>(data) is JArray array)
{
Action<string>? getFiles = default;
void addFiles(JArray array)
void addFiles(JArray array, string relativeFrom)
{
foreach (var item in array)
{
// Write poor man's progress
Console.Write(".");
if ("file".Equals(item["type"]?.ToString(), StringComparison.Ordinal))
{
var itemPath = item["path"]!.ToString();
var itemPath = item["path"]!.ToString().Replace(relativeFrom, "");

// If there is a relative path before the '.', infer the target file name
// but avoid the directory structure
var flatten = baseDir.Split('/', '\\').LastOrDefault() == ".";
Expand All @@ -112,9 +117,14 @@ void addFiles(JArray array)
else if (baseDir.Length > 0 && itemPath.StartsWith(baseDir))
itemPath = itemPath.Substring(baseDir.Length);

files.Add(new FileSpec(
Path.Combine(baseDir == "." ? "" : flatten ? baseDir.TrimEnd('.') : baseDir, itemPath.TrimStart('/'))
.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
files.Add(new FileSpec(Path
.Combine(baseDir == "."
? ""
: flatten
? baseDir.TrimEnd('.')
: baseDir,
itemPath.TrimStart('/'))
.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
// We use the html url since our Http handler knows how to turn those into
// raw URLs, and it's nicer to keep the browsable URL instead so users
// can easily click on that and browse the content on the web.
Expand All @@ -131,10 +141,10 @@ void addFiles(JArray array)
if (Process.TryExecute("gh", "api " + apiUrl + path + apiQuery, out var data) &&
JsonConvert.DeserializeObject<JToken>(data) is JArray array)
{
addFiles(array);
addFiles(array, relativeFrom);
}
};
addFiles(array);
addFiles(array, relativeFrom);
return GitHubResult.Success;
}
else
Expand Down
9 changes: 9 additions & 0 deletions src/Tests/FileSpecTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ public void WhenPathIsDirAppendsDefaultPath(string url, string path, string expe
Assert.Equal(expected, spec.Path);
}

[Fact]
public void WhenSourceUriEndsInSlathThenTargetPathIsBaseDir()
{
var spec = new FileSpec("docs/", new Uri("https://github.com/devlooped/dotnet-file/tree/main/src/api/documentation/"));

// NOTE: the relative `src/api/documentation` is not appended to the file path.
Assert.Equal("docs", spec.Path);
}

[Theory]
[InlineData("https://github.com/devlooped/dotnet-file/blob/main/src/Foo.cs", ".", "Foo.cs")]
[InlineData("https://github.com/devlooped/dotnet-file/blob/main/src/Foo.cs", "src/External/.", "src/External/Foo.cs")]
Expand Down

0 comments on commit cc23040

Please sign in to comment.