Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/.idea/.idea.port/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/.idea/.idea.port/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/.idea/.idea.src.dir/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/.idea/.idea.src.dir/.idea/projectSettingsUpdater.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions src/CommandChainDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace port;

/// <summary>
/// Detects if the current command is part of a command chain by analyzing command execution context.
/// This helps determine when to suppress intermediate output like listing current state.
/// </summary>
internal class CommandChainDetector : ICommandChainDetector
{
private readonly Lazy<bool> _shouldDisplayOutput;

public CommandChainDetector()
{
_shouldDisplayOutput = new Lazy<bool>(DetectShouldDisplayOutput);
}

public bool ShouldDisplayOutput() => _shouldDisplayOutput.Value;

private static bool DetectShouldDisplayOutput()
{
try
{
// Get the current command being executed
var args = Environment.GetCommandLineArgs();
if (args.Length < 2)
return true;

var command = args[1].ToLowerInvariant();

// Commands that typically end chains and should show output by default
var finalCommands = new[] { "prune", "pr", "list", "ls" };

// Commands that are typically intermediate in chains
var intermediateCommands = new[] { "pull", "p", "run", "r" };

// Simple heuristic: if this is a command that typically ends chains, show output
// Otherwise, for intermediate commands, we suppress by default (conservative approach)
if (finalCommands.Contains(command))
return true;

if (intermediateCommands.Contains(command))
return false;

// For other commands (commit, reset, stop, remove, etc.), show output by default
return true;
}
catch
{
// If we can't determine the chain status, default to showing output
return true;
}
}
}
167 changes: 97 additions & 70 deletions src/Commands/Commit/CommitCliCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,83 +14,108 @@ internal class CommitCliCommand(
IGetDigestsByIdQuery getDigestsByIdQuery,
IGetContainersQuery getContainersQuery,
IStopAndRemoveContainerCommand stopAndRemoveContainerCommand,
ListCliCommand listCliCommand)
: AsyncCommand<CommitSettings>
ConditionalListCliCommand conditionalListCliCommand
) : AsyncCommand<CommitSettings>
{
public override async Task<int> ExecuteAsync(CommandContext context, CommitSettings settings)
{
var container = await GetContainerAsync(settings) ??
throw new InvalidOperationException("No running container found");
var container =
await GetContainerAsync(settings)
?? throw new InvalidOperationException("No running container found");

await Spinner.StartAsync("Committing container", async ctx =>
{
string newTag;
string imageName;
string tagPrefix;
if (settings.Overwrite)
{
newTag = container.ImageTag ??
throw new InvalidOperationException(
"When using --overwrite, container must have an image tag");
imageName = container.ImageIdentifier;
tagPrefix = container.TagPrefix;
}
else
await Spinner.StartAsync(
"Committing container",
async ctx =>
{
var tag = settings.Tag ?? $"{DateTime.Now:yyyyMMddhhmmss}";
(imageName, tagPrefix, newTag) = await GetNewTagAsync(container, tag);
}


ctx.Status = $"Looking for existing container named '{container.ContainerName}'";
var containerWithSameTag = await getContainersQuery
.QueryByContainerIdentifierAndTagAsync(container.ContainerIdentifier, newTag)
.ToListAsync();

ctx.Status = $"Creating image from running container '{container.ContainerName}'";
newTag = await createImageFromContainerCommand.ExecuteAsync(container, imageName, tagPrefix, newTag);

ctx.Status = $"Removing containers named '{container.ContainerName}'";
await Task.WhenAll(containerWithSameTag.Select(async container1 =>
await stopAndRemoveContainerCommand.ExecuteAsync(container1.Id)));

if (settings.Overwrite)
{
if (newTag == null)
throw new InvalidOperationException("newTag is null");

if (container.ImageTag == null)
throw new InvalidOperationException(
"Switch argument not supported when creating image from untagged container");

ctx.Status = "Launching new image";
var id = await createContainerCommand.ExecuteAsync(container, tagPrefix, newTag);
await runContainerCommand.ExecuteAsync(id);
}
else if (settings.Switch)
{
if (newTag == null)
throw new InvalidOperationException("newTag is null");

if (container.ImageTag == null)
throw new InvalidOperationException(
"Switch argument not supported when creating image from untagged container");

ctx.Status = $"Stopping running container '{container.ContainerName}'";
await stopContainerCommand.ExecuteAsync(container.Id);

ctx.Status = "Launching new image";
var id = await createContainerCommand.ExecuteAsync(container, tagPrefix, newTag);
await runContainerCommand.ExecuteAsync(id);
string newTag;
string imageName;
string tagPrefix;
if (settings.Overwrite)
{
newTag =
container.ImageTag
?? throw new InvalidOperationException(
"When using --overwrite, container must have an image tag"
);
imageName = container.ImageIdentifier;
tagPrefix = container.TagPrefix;
}
else
{
var tag = settings.Tag ?? $"{DateTime.Now:yyyyMMddhhmmss}";
(imageName, tagPrefix, newTag) = await GetNewTagAsync(container, tag);
}

ctx.Status = $"Looking for existing container named '{container.ContainerName}'";
var containerWithSameTag = await getContainersQuery
.QueryByContainerIdentifierAndTagAsync(container.ContainerIdentifier, newTag)
.ToListAsync();

ctx.Status = $"Creating image from running container '{container.ContainerName}'";
newTag = await createImageFromContainerCommand.ExecuteAsync(
container,
imageName,
tagPrefix,
newTag
);

ctx.Status = $"Removing containers named '{container.ContainerName}'";
await Task.WhenAll(
containerWithSameTag.Select(async container1 =>
await stopAndRemoveContainerCommand.ExecuteAsync(container1.Id)
)
);

if (settings.Overwrite)
{
if (newTag == null)
throw new InvalidOperationException("newTag is null");

if (container.ImageTag == null)
throw new InvalidOperationException(
"Switch argument not supported when creating image from untagged container"
);

ctx.Status = "Launching new image";
var id = await createContainerCommand.ExecuteAsync(
container,
tagPrefix,
newTag
);
await runContainerCommand.ExecuteAsync(id);
}
else if (settings.Switch)
{
if (newTag == null)
throw new InvalidOperationException("newTag is null");

if (container.ImageTag == null)
throw new InvalidOperationException(
"Switch argument not supported when creating image from untagged container"
);

ctx.Status = $"Stopping running container '{container.ContainerName}'";
await stopContainerCommand.ExecuteAsync(container.Id);

ctx.Status = "Launching new image";
var id = await createContainerCommand.ExecuteAsync(
container,
tagPrefix,
newTag
);
await runContainerCommand.ExecuteAsync(id);
}
}
});
);

await listCliCommand.ExecuteAsync();
await conditionalListCliCommand.ExecuteAsync();
return 0;
}

private async Task<(string imageName, string tagPrefix, string newTag)> GetNewTagAsync(Container container,
string tag)
private async Task<(string imageName, string tagPrefix, string newTag)> GetNewTagAsync(
Container container,
string tag
)
{
var image = await getImageQuery.QueryAsync(container.ImageIdentifier, container.ImageTag);
string imageName;
Expand All @@ -101,7 +126,8 @@ await Task.WhenAll(containerWithSameTag.Select(async container1 =>
var digest = digests?.SingleOrDefault();
if (digest == null || !DigestHelper.TryGetImageNameAndId(digest, out var nameNameAndId))
throw new InvalidOperationException(
$"Unable to determine image name from running container '{container.ContainerName}'");
$"Unable to determine image name from running container '{container.ContainerName}'"
);
imageName = nameNameAndId.imageName;
}
else
Expand All @@ -114,7 +140,8 @@ await Task.WhenAll(containerWithSameTag.Select(async container1 =>

var tagPrefix = container.TagPrefix;
var newTag = baseTag == null ? tag : $"{tagPrefix}{baseTag}-{tag}";
if (newTag.Contains('.')) throw new ArgumentException("only [a-zA-Z0-9][a-zA-Z0-9_-] are allowed");
if (newTag.Contains('.'))
throw new ArgumentException("only [a-zA-Z0-9][a-zA-Z0-9_-] are allowed");
return (imageName, tagPrefix, newTag);
}

Expand All @@ -129,4 +156,4 @@ await Task.WhenAll(containerWithSameTag.Select(async container1 =>
var identifier = containerNamePrompt.GetIdentifierOfContainerFromUser(containers, "commit");
return containers.SingleOrDefault(c => c.ContainerName == identifier);
}
}
}
8 changes: 4 additions & 4 deletions src/Commands/Commit/CommitSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ public class CommitSettings : CommandSettings, IContainerIdentifierSettings
[CommandArgument(0, "[ContainerIdentifier]")]
public string? ContainerIdentifier { get; set; }

[CommandOption("-t|--tag")]
[CommandOption("-t|--tag")]
public string? Tag { get; set; }

[CommandOption("-s|--switch")]
[CommandOption("-s|--switch")]
public bool Switch { get; set; }

[CommandOption("-o|--overwrite")]
[CommandOption("-o|--overwrite")]
public bool Overwrite { get; set; }
}
}
36 changes: 22 additions & 14 deletions src/Commands/Commit/CreateImageFromContainerCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,35 @@

namespace port.Commands.Commit;

internal class CreateImageFromContainerCommand(IDockerClient dockerClient) : ICreateImageFromContainerCommand
internal class CreateImageFromContainerCommand(IDockerClient dockerClient)
: ICreateImageFromContainerCommand
{
public async Task<string> ExecuteAsync(Container container, string imageName, string tagPrefix, string newTag)
public async Task<string> ExecuteAsync(
Container container,
string imageName,
string tagPrefix,
string newTag
)
{
var labels = new Dictionary<string, string>();
var identifier = container.GetLabel(Constants.IdentifierLabel);
if (identifier is not null) labels.Add(Constants.IdentifierLabel, identifier);
if (identifier is not null)
labels.Add(Constants.IdentifierLabel, identifier);
var baseTag = container.GetLabel(Constants.BaseTagLabel);
if (baseTag is not null) labels.Add(Constants.BaseTagLabel, baseTag);
if (baseTag is not null)
labels.Add(Constants.BaseTagLabel, baseTag);
labels.Add(Constants.TagPrefix, tagPrefix);
if (baseTag == newTag) throw new InvalidOperationException("Can not overwrite base tags");
await dockerClient.Images.CommitContainerChangesAsync(new CommitContainerChangesParameters
{
ContainerID = container.Id,
RepositoryName = imageName,
Tag = newTag,
Config = new Docker.DotNet.Models.Config
if (baseTag == newTag)
throw new InvalidOperationException("Can not overwrite base tags");
await dockerClient.Images.CommitContainerChangesAsync(
new CommitContainerChangesParameters
{
Labels = labels
ContainerID = container.Id,
RepositoryName = imageName,
Tag = newTag,
Config = new Docker.DotNet.Models.Config { Labels = labels },
}
});
);
return newTag;
}
}
}
7 changes: 3 additions & 4 deletions src/Commands/Commit/GetDigestsByIdQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ public GetDigestsByIdQuery(IDockerClient dockerClient)
{
var parameters = new ImagesListParameters
{
Filters = new Dictionary<string, IDictionary<string, bool>>()
Filters = new Dictionary<string, IDictionary<string, bool>>(),
};
var imagesListResponses = await _dockerClient.Images.ListImagesAsync(parameters);
var imagesListResponse = imagesListResponses
.SingleOrDefault(e => e.ID == imageId);
var imagesListResponse = imagesListResponses.SingleOrDefault(e => e.ID == imageId);
return imagesListResponse?.RepoDigests;
}
}
}
9 changes: 7 additions & 2 deletions src/Commands/Commit/ICreateImageFromContainerCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@ namespace port.Commands.Commit;

public interface ICreateImageFromContainerCommand
{
Task<string> ExecuteAsync(Container container, string imageName, string tagPrefix, string newTag);
}
Task<string> ExecuteAsync(
Container container,
string imageName,
string tagPrefix,
string newTag
);
}
2 changes: 1 addition & 1 deletion src/Commands/Commit/IGetDigestsByIdQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ namespace port.Commands.Commit;
internal interface IGetDigestsByIdQuery
{
Task<IList<string>?> QueryAsync(string imageId);
}
}
5 changes: 3 additions & 2 deletions src/Commands/Config/ConfigCliCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ public override int Execute(CommandContext context, ConfigSettings settings)
return 0;
}

private static string FormatAsLink(string caption, string url) => $"\u001B]8;;{url}\a{caption}\u001B]8;;\a";
}
private static string FormatAsLink(string caption, string url) =>
$"\u001B]8;;{url}\a{caption}\u001B]8;;\a";
}
Loading
Loading