Skip to content

Commit

Permalink
support gitignore like file for excluding files from deployment. Closes
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmelsayed committed Feb 9, 2018
1 parent 6498df4 commit 5011441
Show file tree
Hide file tree
Showing 7 changed files with 383 additions and 39 deletions.
163 changes: 124 additions & 39 deletions src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
using Azure.Functions.Cli.Interfaces;
using Colors.Net;
using Fclp;
using Microsoft.Azure.WebJobs.Script;
using static Azure.Functions.Cli.Common.OutputTheme;

namespace Azure.Functions.Cli.Actions.AzureActions
Expand All @@ -29,6 +28,8 @@ internal class PublishFunctionApp : BaseFunctionAppAction
public bool PublishLocalSettings { get; set; }
public bool OverwriteSettings { get; set; }
public bool PublishLocalSettingsOnly { get; set; }
public bool ListIgnoredFiles { get; set; }
public bool ListIncludedFiles { get; set; }

public PublishFunctionApp(IArmManager armManager, ISettings settings, ISecretsManager secretsManager)
: base(armManager)
Expand All @@ -51,62 +52,143 @@ public override ICommandLineParserResult ParseArgs(string[] args)
.Setup<bool>('y', "overwrite-settings")
.WithDescription("Only to be used in conjunction with -i or -o. Overwrites AppSettings in Azure with local value if different. Default is prompt.")
.Callback(f => OverwriteSettings = f);
Parser
.Setup<bool>("list-ignored-files")
.WithDescription("Displays a list of files that will be ignored from publishing based on .funcignore")
.Callback(f => ListIgnoredFiles = f);
Parser
.Setup<bool>("list-included-files")
.WithDescription("Displays a list of files that will be included in publishing based on .funcignore")
.Callback(f => ListIncludedFiles = f);

return base.ParseArgs(args);
}

public override async Task RunAsync()
{
ColoredConsole.WriteLine("Getting site publishing info...");
var functionApp = await _armManager.GetFunctionAppAsync(FunctionAppName);
if (PublishLocalSettingsOnly)
GitIgnoreParser ignoreParser = null;
try
{
var isSuccessful = await PublishAppSettings(functionApp);
if (!isSuccessful)
var path = Path.Combine(Environment.CurrentDirectory, Constants.FuncIgnoreFile);
if (FileSystemHelpers.FileExists(path))
{
return;
ignoreParser = new GitIgnoreParser(FileSystemHelpers.ReadAllTextFromFile(path));
}
}
catch { }

if (ListIncludedFiles)
{
InternalListIncludedFiles(ignoreParser);
}
else if (ListIgnoredFiles)
{
InternalListIgnoredFiles(ignoreParser);
}
else
{
var functionAppRoot = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory);
ColoredConsole.WriteLine(WarningColor($"Publish {functionAppRoot} contents to an Azure Function App. Locally deleted files are not removed from destination."));
await RetryHelper.Retry(async () =>
if (PublishLocalSettingsOnly)
{
using (var client = await GetRemoteZipClient(new Uri($"https://{functionApp.ScmUri}")))
using (var request = new HttpRequestMessage(HttpMethod.Put, new Uri("api/zip/site/wwwroot", UriKind.Relative)))
{
request.Headers.IfMatch.Add(EntityTagHeaderValue.Any);
await InternalPublishLocalSettingsOnly();
}
else
{
await InternalPublishFunctionApp(ignoreParser);
}
}
}

private async Task InternalPublishFunctionApp(GitIgnoreParser ignoreParser)
{
ColoredConsole.WriteLine("Getting site publishing info...");
var functionApp = await _armManager.GetFunctionAppAsync(FunctionAppName);
var functionAppRoot = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory);
ColoredConsole.WriteLine(WarningColor($"Publish {functionAppRoot} contents to an Azure Function App. Locally deleted files are not removed from destination."));
await RetryHelper.Retry(async () =>
{
using (var client = await GetRemoteZipClient(new Uri($"https://{functionApp.ScmUri}")))
using (var request = new HttpRequestMessage(HttpMethod.Put, new Uri("api/zip/site/wwwroot", UriKind.Relative)))
{
request.Headers.IfMatch.Add(EntityTagHeaderValue.Any);
ColoredConsole.WriteLine("Creating archive for current directory...");
ColoredConsole.WriteLine("Creating archive for current directory...");
request.Content = CreateZip(functionAppRoot);
request.Content = CreateZip(functionAppRoot, ignoreParser);
ColoredConsole.WriteLine("Uploading archive...");
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
throw new CliException($"Error uploading archive ({response.StatusCode}).");
}
ColoredConsole.WriteLine("Uploading archive...");
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
throw new CliException($"Error uploading archive ({response.StatusCode}).");
}
response = await client.PostAsync("api/functions/synctriggers", content: null);
if (!response.IsSuccessStatusCode)
{
throw new CliException($"Error calling sync triggers ({response.StatusCode}).");
}
response = await client.PostAsync("api/functions/synctriggers", content: null);
if (!response.IsSuccessStatusCode)
{
throw new CliException($"Error calling sync triggers ({response.StatusCode}).");
}
if (PublishLocalSettings)
if (PublishLocalSettings)
{
var isSuccessful = await PublishAppSettings(functionApp);
if (!isSuccessful)
{
var isSuccessful = await PublishAppSettings(functionApp);
if (!isSuccessful)
{
return;
}
return;
}
ColoredConsole.WriteLine("Upload completed successfully.");
}
}, 2);
ColoredConsole.WriteLine("Upload completed successfully.");
}
}, 2);
}

private static IEnumerable<string> GetFiles(string path)
{
return FileSystemHelpers.GetFiles(path, new[] { ".git", ".vscode" }, new[] { ".funcignore", ".gitignore", "appsettings.json", "local.settings.json", "project.lock.json" });
}

private async Task InternalPublishLocalSettingsOnly()
{
ColoredConsole.WriteLine("Getting site publishing info...");
var functionApp = await _armManager.GetFunctionAppAsync(FunctionAppName);
var isSuccessful = await PublishAppSettings(functionApp);
if (!isSuccessful)
{
return;
}
}

private static void InternalListIgnoredFiles(GitIgnoreParser ignoreParser)
{
if (ignoreParser == null)
{
ColoredConsole.Error.WriteLine("No .funcignore file");
return;
}

foreach (var file in GetFiles(Environment.CurrentDirectory).Select(f => f.Replace(Environment.CurrentDirectory, "").Trim(Path.DirectorySeparatorChar).Replace("\\", "/")))
{
if (ignoreParser.Denies(file))
{
ColoredConsole.WriteLine(file);
}
}
}

private static void InternalListIncludedFiles(GitIgnoreParser ignoreParser)
{
if (ignoreParser == null)
{
ColoredConsole.Error.WriteLine("No .funcignore file");
return;
}

foreach (var file in GetFiles(Environment.CurrentDirectory).Select(f => f.Replace(Environment.CurrentDirectory, "").Trim(Path.DirectorySeparatorChar).Replace("\\", "/")))
{
if (ignoreParser.Accepts(file))
{
ColoredConsole.WriteLine(file);
}
}
}

Expand Down Expand Up @@ -173,14 +255,17 @@ private IDictionary<string, string> MergeAppSettings(IDictionary<string, string>
return result;
}

private static StreamContent CreateZip(string path)
private static StreamContent CreateZip(string path, GitIgnoreParser ignoreParser)
{
var memoryStream = new MemoryStream();
using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
{
foreach (var fileName in FileSystemHelpers.GetFiles(path, new[] { ".git", ".vscode" }, new[] { ".gitignore", "appsettings.json", "local.settings.json", "project.lock.json" }))
foreach (var fileName in GetFiles(path))
{
zip.AddFile(fileName, fileName, path);
if (ignoreParser?.Accepts(fileName.Replace(path, "").Trim(Path.DirectorySeparatorChar).Replace("\\", "/")) ?? true)
{
zip.AddFile(fileName, fileName, path);
}
}
}
memoryStream.Seek(0, SeekOrigin.Begin);
Expand Down
1 change: 1 addition & 0 deletions src/Azure.Functions.Cli/Azure.Functions.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@
<Compile Include="Common\ExitCodes.cs" />
<Compile Include="Common\FileSystemHelpers.cs" />
<Compile Include="Common\FunctionNotFoundException.cs" />
<Compile Include="Common\GitIgnoreParser.cs" />
<Compile Include="Common\HostStartSettings.cs" />
<Compile Include="Common\HttpResult.cs" />
<Compile Include="Common\OutputTheme.cs" />
Expand Down
1 change: 1 addition & 0 deletions src/Azure.Functions.Cli/Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal static class Constants
public const string DefaultSqlProviderName = "System.Data.SqlClient";
public const string WebsiteHostname = "WEBSITE_HOSTNAME";
public const string ProxiesFileName = "proxies.json";
public const string FuncIgnoreFile = ".funcignore";

public static class Errors
{
Expand Down
5 changes: 5 additions & 0 deletions src/Azure.Functions.Cli/Common/FileSystemHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ internal static IEnumerable<string> GetFiles(string directoryPath, IEnumerable<s
}
}

internal static bool FileExists(object funcIgnoreFile)
{
throw new NotImplementedException();
}

internal static IEnumerable<string> GetDirectories(string scriptPath)
{
return Instance.Directory.GetDirectories(scriptPath);
Expand Down
121 changes: 121 additions & 0 deletions src/Azure.Functions.Cli/Common/GitIgnoreParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Azure.Functions.Cli.Common
{
/// <summary>
/// This is a C# reimplementation of https://github.com/codemix/gitignore-parser
/// License for gitignore-parser:
/// # Copyright 2014 codemix ltd.
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
/// </summary>
internal class GitIgnoreParser
{
private readonly Regex[] _negative;
private readonly Regex[] _positive;

public GitIgnoreParser(string gitIgnoreContent)
{
var parsed = gitIgnoreContent
.Split('\n')
.Select(l => l.Trim())
.Where(l => !l.StartsWith("#"))
.Where(l => !string.IsNullOrEmpty(l))
.Aggregate(new List<List<string>>() { new List<string>(), new List<string>() }, (lists, line) =>
{
var isNegative = line.StartsWith("!");
if (isNegative)
{
line = line.Substring(1);
}
if (line.StartsWith("/"))
{
line = line.Substring(1);
}
if (isNegative)
{
lists[1].Add(line);
}
else
{
lists[0].Add(line);
}
return lists;
})
.Select(l =>
{
return l
.OrderBy(i => i)
.Select(i => new[] { PrepareRegexPattern(i), PreparePartialRegex(i) })
.Aggregate(new List<List<string>>() { new List<string>(), new List<string>() }, (list, prepared) =>
{
list[0].Add(prepared[0]);
list[1].Add(prepared[1]);
return list;
});
})
.Select(item => new[]
{
item[0].Count > 0 ? new Regex("^((" + string.Join(")|(", item[0]) + "))", RegexOptions.ECMAScript) : new Regex("$^", RegexOptions.ECMAScript),
item[1].Count > 0 ? new Regex("^((" + string.Join(")|(", item[1]) + "))", RegexOptions.ECMAScript) : new Regex("$^", RegexOptions.ECMAScript)
})
.ToArray();
_positive = parsed[0];
_negative = parsed[1];
}

public bool Accepts(string input)
{
if (input == "/")
{
input = input.Substring(1);
}
return _negative[0].IsMatch(input) || !_positive[0].IsMatch(input);
}

public bool Denies(string input)
{
if (input == "/")
{
input = input.Substring(1);
}
return !(_negative[0].IsMatch(input) || !_positive[0].IsMatch(input));
}

private string PrepareRegexPattern(string pattern)
{
return Regex.Replace(pattern, @"[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]", "\\$&", RegexOptions.ECMAScript)
.Replace("**", "(.+)")
.Replace("*", "([^\\/]+)");
}

private string PreparePartialRegex(string pattern)
{
return pattern
.Split('/')
.Select((item, index) =>
{
if (index == 0)
{
return "([\\/]?(" + PrepareRegexPattern(item) + "\\b|$))";
}
else
{
return "(" + PrepareRegexPattern(item) + "\\b)";
}
})
.Aggregate((a, b) => a + b);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@
<Compile Include="ExtensionsTests\ProcessExtensionsTests.cs" />
<Compile Include="ExtensionsTests\TaskExtensionsTests.cs" />
<Compile Include="ExtensionsTests\UriExtensionsTests.cs" />
<Compile Include="GitIgnoreParserTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
Expand Down
Loading

0 comments on commit 5011441

Please sign in to comment.