Skip to content

Commit

Permalink
[cli] Render changes to text properties as diffs. (#9376)
Browse files Browse the repository at this point in the history
Instead of simply rendering the old and new values for textual
properties, compute the diff between the old and new values and render
that. For single-line values, the diff is computed and rendered
character-wise, with simplification applied to avoid "small" diffs. For
multi-line values, the diff is computed and rendered linewise (as we
already to for textual assets).

Fixes #9136.
  • Loading branch information
pgavlin committed Apr 11, 2022
1 parent 5528cde commit 21b9f29
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG_PENDING.md
Expand Up @@ -12,6 +12,9 @@
- [cli] - Warn when `additionalSecretOutputs` is used to mark the `id` property as secret.
[#9360](https://github.com/pulumi/pulumi/pull/9360)

- [cli] Display richer diffs for texutal property values.
[#9376](https://github.com/pulumi/pulumi/pull/9376)

### Bug Fixes

- [codegen/node] - Fix an issue with escaping deprecation messages.
Expand Down
71 changes: 60 additions & 11 deletions pkg/backend/display/object_diff.go
Expand Up @@ -767,6 +767,12 @@ func (p *propertyPrinter) printPropertyValueDiff(titleFunc func(*propertyPrinter

if isPrimitive(diff.Old) && isPrimitive(diff.New) {
titleFunc(p)

if diff.Old.IsString() && diff.New.IsString() {
p.printTextDiff(diff.Old.StringValue(), diff.New.StringValue())
return
}

p.withOp(deploy.OpDelete).printPrimitivePropertyValue(diff.Old)
p.writeVerbatim(" => ")
p.withOp(deploy.OpCreate).printPrimitivePropertyValue(diff.New)
Expand Down Expand Up @@ -985,19 +991,12 @@ func (p *propertyPrinter) printAssetDiff(titleFunc func(*propertyPrinter), oldAs
if oldAsset.IsText() {
if newAsset.IsText() {
titleFunc(p)
p.write("asset(text:%s) {\n", hashChange)
p.write("asset(text:%s) {", hashChange)

massagedOldText := resource.MassageIfUserProgramCodeAsset(oldAsset, p.debug).Text
massagedNewText := resource.MassageIfUserProgramCodeAsset(newAsset, p.debug).Text

differ := diffmatchpatch.New()
differ.DiffTimeout = 0

hashed1, hashed2, lineArray := differ.DiffLinesToChars(massagedOldText, massagedNewText)
diffs1 := differ.DiffMain(hashed1, hashed2, false)
diffs2 := differ.DiffCharsToLines(diffs1, lineArray)

p.indented(1).prettyPrintDiff(diffs2)
p.indented(1).printTextDiff(massagedOldText, massagedNewText)

p.writeWithIndentNoPrefix("}\n")
return
Expand Down Expand Up @@ -1037,12 +1036,62 @@ func getTextChangeString(old string, new string) string {
return fmt.Sprintf("%s->%s", old, new)
}

// prettyPrintDiff takes the full diff produed by diffmatchpatch and condenses it into something
func escape(s string) string {
escaped := strconv.Quote(s)
return escaped[1 : len(escaped)-1]
}

func (p *propertyPrinter) printTextDiff(old, new string) {
differ := diffmatchpatch.New()
differ.DiffTimeout = 0

singleLine := !strings.ContainsRune(old, '\n') && !strings.ContainsRune(new, '\n')
if singleLine {
diff := differ.DiffMain(old, new, false)
p.printCharacterDiff(differ.DiffCleanupEfficiency(diff))
} else {
hashed1, hashed2, lineArray := differ.DiffLinesToChars(old, new)
diffs := differ.DiffMain(hashed1, hashed2, false)
p.indented(1).printLineDiff(differ.DiffCharsToLines(diffs, lineArray))
}
}

func (p *propertyPrinter) printCharacterDiff(diffs []diffmatchpatch.Diff) {
// write the old text.
p.writeVerbatim(`"`)
for _, d := range diffs {
switch d.Type {
case diffmatchpatch.DiffDelete:
p.withOp(deploy.OpDelete).write(escape(d.Text))
case diffmatchpatch.DiffEqual:
p.withOp(deploy.OpSame).write(escape(d.Text))
}
}
p.writeVerbatim(`"`)

p.writeVerbatim(" => ")

// write the new text.
p.writeVerbatim(`"`)
for _, d := range diffs {
switch d.Type {
case diffmatchpatch.DiffInsert:
p.withOp(deploy.OpCreate).write(escape(d.Text))
case diffmatchpatch.DiffEqual:
p.withOp(deploy.OpSame).write(escape(d.Text))
}
}
p.writeVerbatim("\"\n")
}

// printLineDiff takes the full diff produed by diffmatchpatch and condenses it into something
// useful we can print to the console. Specifically, while it includes any adds/removes in
// green/red, it will also show portions of the unchanged text to help give surrounding context to
// those add/removes. Because the unchanged portions may be very large, it only included around 3
// lines before/after the change.
func (p *propertyPrinter) prettyPrintDiff(diffs []diffmatchpatch.Diff) {
func (p *propertyPrinter) printLineDiff(diffs []diffmatchpatch.Diff) {
p.writeVerbatim("\n")

writeDiff := func(op deploy.StepOp, text string) {
prefix := op == deploy.OpCreate || op == deploy.OpDelete
p.withOp(op).withPrefix(prefix).writeWithIndent("%s", text)
Expand Down
2 changes: 1 addition & 1 deletion pkg/backend/display/testdata/up.json.stdout.txt
Expand Up @@ -6,7 +6,7 @@
<{%reset%}> [id=eks-role-24b1266]
<{%reset%}><{%reset%}> [urn=urn:pulumi:dev::eks::aws:iam/role:Role::eks-role]
<{%reset%}><{%reset%}> [provider=urn:pulumi:dev::eks::pulumi:providers:aws::default_4_36_0::7b99a6ae-83b6-49d1-a82d-3f9f7cf83d42]
<{%reset%}><{%fg 3%}> ~ assumeRolePolicy : <{%reset%}><{%fg 1%}>"{\"Version\":\"2008-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"eks.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"<{%reset%}><{%fg 3%}> => <{%reset%}><{%fg 2%}>"{\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"eks.amazonaws.com\"},\"Sid\":\"\"}],\"Version\":\"2008-10-17\"}"<{%reset%}><{%fg 3%}>
<{%reset%}><{%fg 3%}> ~ assumeRolePolicy : <{%reset%}><{%fg 3%}>"<{%reset%}><{%reset%}>{\"<{%reset%}><{%fg 1%}>Version\":\"2008-10-17\",\"<{%reset%}><{%reset%}>Statement\":[{\"<{%reset%}><{%fg 1%}>Sid<{%reset%}><{%reset%}>\":\"<{%reset%}><{%reset%}>\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"eks.amazonaws.com\"},\"<{%reset%}><{%fg 1%}>Action<{%reset%}><{%reset%}>\":\"<{%reset%}><{%fg 1%}>sts:AssumeRole<{%reset%}><{%reset%}>\"}]<{%reset%}><{%reset%}>}<{%reset%}><{%fg 3%}>"<{%reset%}><{%fg 3%}> => <{%reset%}><{%fg 3%}>"<{%reset%}><{%reset%}>{\"<{%reset%}><{%reset%}>Statement\":[{\"<{%reset%}><{%fg 2%}>Action<{%reset%}><{%reset%}>\":\"<{%reset%}><{%fg 2%}>sts:AssumeRole<{%reset%}><{%reset%}>\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"eks.amazonaws.com\"},\"<{%reset%}><{%fg 2%}>Sid<{%reset%}><{%reset%}>\":\"<{%reset%}><{%reset%}>\"}]<{%reset%}><{%fg 2%}>,\"Version\":\"2008-10-17\"<{%reset%}><{%reset%}>}<{%reset%}><{%fg 3%}>"
<{%reset%}><{%reset%}> forceDetachPolicies: <{%reset%}><{%reset%}>false<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> maxSessionDuration : <{%reset%}><{%reset%}>3600<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> name : <{%reset%}><{%reset%}>"eks-role-24b1266"<{%reset%}><{%reset%}>
Expand Down
14 changes: 14 additions & 0 deletions pkg/backend/display/testdata/webserver-userdata.json

Large diffs are not rendered by default.

Empty file.
Empty file.
66 changes: 66 additions & 0 deletions pkg/backend/display/testdata/webserver-userdata.json.stdout
@@ -0,0 +1,66 @@
<{%reset%}>Configuration:<{%reset%}>
aws:region: us-west-2
<{%reset%}> pulumi:pulumi:Stack: (same)
<{%reset%}> [urn=urn:pulumi:dev::aws-ts-webserver::pulumi:pulumi:Stack::aws-ts-webserver-dev]
<{%reset%}><{%reset%}><{%!r(MISSING)eset%!}(MISSING)> --outputs:--<{%!r(MISSING)eset%!}(MISSING)>
<{%reset%}> publicHostName: <{%reset%}><{%reset%}>"ec2-34-211-56-110.us-west-2.compute.amazonaws.com"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> publicIp : <{%reset%}><{%reset%}>"34.211.56.110"<{%reset%}><{%reset%}>
<{%reset%}> <{%reset%}> aws:ec2/securityGroup:SecurityGroup: (same)
<{%reset%}> [id=sg-07498abcdbdf88f34]
<{%reset%}><{%reset%}> [urn=urn:pulumi:dev::aws-ts-webserver::aws:ec2/securityGroup:SecurityGroup::web-secgrp]
<{%reset%}><{%reset%}> [provider=urn:pulumi:dev::aws-ts-webserver::pulumi:providers:aws::default_3_38_1::57baf899-740a-486a-908e-43cf27cce182]
<{%reset%}><{%reset%}> description : <{%reset%}><{%reset%}>"Managed by Pulumi"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> ingress : <{%reset%}><{%reset%}>[
<{%reset%}><{%reset%}> [0]: <{%reset%}><{%reset%}>{
<{%reset%}><{%reset%}> cidrBlocks: <{%reset%}><{%reset%}>[
<{%reset%}><{%reset%}> [0]: <{%reset%}><{%reset%}>"0.0.0.0/0"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> ]<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> fromPort : <{%reset%}><{%reset%}>80<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> protocol : <{%reset%}><{%reset%}>"tcp"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> self : <{%reset%}><{%reset%}>false<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> toPort : <{%reset%}><{%reset%}>80<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> }<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> ]<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> name : <{%reset%}><{%reset%}>"web-secgrp-5ee4db6"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> revokeRulesOnDelete: <{%reset%}><{%reset%}>false<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> <{%fg 10%}>++aws:ec2/instance:Instance: (create-replacement)
<{%reset%}> [id=i-0207dc7a2d8c5135a]
<{%reset%}><{%reset%}> [urn=urn:pulumi:dev::aws-ts-webserver::aws:ec2/instance:Instance::web-server-www]
<{%reset%}><{%reset%}> [provider=urn:pulumi:dev::aws-ts-webserver::pulumi:providers:aws::default_3_38_1::57baf899-740a-486a-908e-43cf27cce182]
<{%reset%}><{%fg 3%}> ~ userData: <{%reset%}><{%fg 3%}>
<{%reset%}><{%fg 1%}> - 6c9af18dedd883b14ac2969ec364e7113dd8533c<{%reset%}>
<{%fg 2%}> + #!/bin/bash<{%reset%}>
<{%fg 2%}> + echo "Hello, Pulumi!" > index.html<{%reset%}>
<{%fg 2%}> + nohup python -m SimpleHTTPServer 80 &<{%reset%}>
<{%reset%}> <{%fg 13%}>+-aws:ec2/instance:Instance: (replace)
<{%reset%}> [id=i-0207dc7a2d8c5135a]
<{%reset%}><{%reset%}> [urn=urn:pulumi:dev::aws-ts-webserver::aws:ec2/instance:Instance::web-server-www]
<{%reset%}><{%reset%}> [provider=urn:pulumi:dev::aws-ts-webserver::pulumi:providers:aws::default_3_38_1::57baf899-740a-486a-908e-43cf27cce182]
<{%reset%}><{%fg 3%}> ~ userData: <{%reset%}><{%fg 3%}>
<{%reset%}><{%fg 1%}> - 6c9af18dedd883b14ac2969ec364e7113dd8533c<{%reset%}>
<{%fg 2%}> + #!/bin/bash<{%reset%}>
<{%fg 2%}> + echo "Hello, Pulumi!" > index.html<{%reset%}>
<{%fg 2%}> + nohup python -m SimpleHTTPServer 80 &<{%reset%}>
<{%reset%}><{%!r(MISSING)eset%!}(MISSING)> --outputs:--<{%!r(MISSING)eset%!}(MISSING)>
<{%fg 3%}> ~ publicHostName: <{%reset%}><{%fg 1%}>"ec2-34-211-56-110.us-west-2.compute.amazonaws.com"<{%reset%}><{%fg 3%}> => <{%reset%}><{%fg 2%}>output<string><{%reset%}><{%fg 3%}>
<{%reset%}><{%fg 3%}> ~ publicIp : <{%reset%}><{%fg 1%}>"34.211.56.110"<{%reset%}><{%fg 3%}> => <{%reset%}><{%fg 2%}>output<string><{%reset%}><{%fg 3%}>
<{%reset%}> <{%fg 9%}>--aws:ec2/instance:Instance: (delete-replaced)
<{%fg 9%}> [id=i-0207dc7a2d8c5135a]
<{%reset%}><{%fg 9%}> [urn=urn:pulumi:dev::aws-ts-webserver::aws:ec2/instance:Instance::web-server-www]
<{%reset%}><{%fg 9%}> [provider=urn:pulumi:dev::aws-ts-webserver::pulumi:providers:aws::default_3_38_1::57baf899-740a-486a-908e-43cf27cce182]
<{%reset%}><{%fg 9%}> ami : <{%reset%}><{%fg 9%}>"ami-0175af5baaf2bce8e"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> getPasswordData : <{%reset%}><{%fg 9%}>false<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> instanceType : <{%reset%}><{%fg 9%}>"t2.micro"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> sourceDestCheck : <{%reset%}><{%fg 9%}>true<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> tags : <{%reset%}><{%fg 9%}>{
<{%reset%}><{%fg 9%}> Name : <{%reset%}><{%fg 9%}>"web-server-www"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> }<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> userData : <{%reset%}><{%fg 9%}>"#!/bin/bash\necho \"Hello, World!\" > index.html\nnohup python -m SimpleHTTPServer 80 &"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> vpcSecurityGroupIds: <{%reset%}><{%fg 9%}>[
<{%reset%}><{%fg 9%}> [0]: <{%reset%}><{%fg 9%}>"sg-07498abcdbdf88f34"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> ]<{%reset%}><{%fg 9%}>
<{%reset%}><{%reset%}><{%fg 13%}><{%bold%}>Resources:<{%reset%}>
<{%fg 13%}>+-1 replaced<{%reset%}>
2 unchanged

<{%fg 13%}><{%bold%}>Duration:<{%reset%}> 12s
66 changes: 66 additions & 0 deletions pkg/backend/display/testdata/webserver-userdata.json.stdout.txt
@@ -0,0 +1,66 @@
<{%reset%}>Configuration:<{%reset%}>
aws:region: us-west-2
<{%reset%}> pulumi:pulumi:Stack: (same)
<{%reset%}> [urn=urn:pulumi:dev::aws-ts-webserver::pulumi:pulumi:Stack::aws-ts-webserver-dev]
<{%reset%}><{%reset%}><{%!r(MISSING)eset%!}(MISSING)> --outputs:--<{%!r(MISSING)eset%!}(MISSING)>
<{%reset%}> publicHostName: <{%reset%}><{%reset%}>"ec2-34-211-56-110.us-west-2.compute.amazonaws.com"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> publicIp : <{%reset%}><{%reset%}>"34.211.56.110"<{%reset%}><{%reset%}>
<{%reset%}> <{%reset%}> aws:ec2/securityGroup:SecurityGroup: (same)
<{%reset%}> [id=sg-07498abcdbdf88f34]
<{%reset%}><{%reset%}> [urn=urn:pulumi:dev::aws-ts-webserver::aws:ec2/securityGroup:SecurityGroup::web-secgrp]
<{%reset%}><{%reset%}> [provider=urn:pulumi:dev::aws-ts-webserver::pulumi:providers:aws::default_3_38_1::57baf899-740a-486a-908e-43cf27cce182]
<{%reset%}><{%reset%}> description : <{%reset%}><{%reset%}>"Managed by Pulumi"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> ingress : <{%reset%}><{%reset%}>[
<{%reset%}><{%reset%}> [0]: <{%reset%}><{%reset%}>{
<{%reset%}><{%reset%}> cidrBlocks: <{%reset%}><{%reset%}>[
<{%reset%}><{%reset%}> [0]: <{%reset%}><{%reset%}>"0.0.0.0/0"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> ]<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> fromPort : <{%reset%}><{%reset%}>80<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> protocol : <{%reset%}><{%reset%}>"tcp"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> self : <{%reset%}><{%reset%}>false<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> toPort : <{%reset%}><{%reset%}>80<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> }<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> ]<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> name : <{%reset%}><{%reset%}>"web-secgrp-5ee4db6"<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> revokeRulesOnDelete: <{%reset%}><{%reset%}>false<{%reset%}><{%reset%}>
<{%reset%}><{%reset%}> <{%fg 10%}>++aws:ec2/instance:Instance: (create-replacement)
<{%reset%}> [id=i-0207dc7a2d8c5135a]
<{%reset%}><{%reset%}> [urn=urn:pulumi:dev::aws-ts-webserver::aws:ec2/instance:Instance::web-server-www]
<{%reset%}><{%reset%}> [provider=urn:pulumi:dev::aws-ts-webserver::pulumi:providers:aws::default_3_38_1::57baf899-740a-486a-908e-43cf27cce182]
<{%reset%}><{%fg 3%}> ~ userData: <{%reset%}><{%fg 3%}>
<{%reset%}><{%reset%}> #!/bin/bash<{%reset%}>
<{%fg 1%}> - echo "Hello, World!" > index.html<{%reset%}>
<{%fg 2%}> + echo "Hello, Pulumi!" > index.html<{%reset%}>
<{%reset%}> nohup python -m SimpleHTTPServer 80 &<{%reset%}>
<{%reset%}> <{%fg 13%}>+-aws:ec2/instance:Instance: (replace)
<{%reset%}> [id=i-0207dc7a2d8c5135a]
<{%reset%}><{%reset%}> [urn=urn:pulumi:dev::aws-ts-webserver::aws:ec2/instance:Instance::web-server-www]
<{%reset%}><{%reset%}> [provider=urn:pulumi:dev::aws-ts-webserver::pulumi:providers:aws::default_3_38_1::57baf899-740a-486a-908e-43cf27cce182]
<{%reset%}><{%fg 3%}> ~ userData: <{%reset%}><{%fg 3%}>
<{%reset%}><{%reset%}> #!/bin/bash<{%reset%}>
<{%fg 1%}> - echo "Hello, World!" > index.html<{%reset%}>
<{%fg 2%}> + echo "Hello, Pulumi!" > index.html<{%reset%}>
<{%reset%}> nohup python -m SimpleHTTPServer 80 &<{%reset%}>
<{%reset%}><{%!r(MISSING)eset%!}(MISSING)> --outputs:--<{%!r(MISSING)eset%!}(MISSING)>
<{%fg 3%}> ~ publicHostName: <{%reset%}><{%fg 1%}>"ec2-34-211-56-110.us-west-2.compute.amazonaws.com"<{%reset%}><{%fg 3%}> => <{%reset%}><{%fg 2%}>output<string><{%reset%}><{%fg 3%}>
<{%reset%}><{%fg 3%}> ~ publicIp : <{%reset%}><{%fg 1%}>"34.211.56.110"<{%reset%}><{%fg 3%}> => <{%reset%}><{%fg 2%}>output<string><{%reset%}><{%fg 3%}>
<{%reset%}> <{%fg 9%}>--aws:ec2/instance:Instance: (delete-replaced)
<{%fg 9%}> [id=i-0207dc7a2d8c5135a]
<{%reset%}><{%fg 9%}> [urn=urn:pulumi:dev::aws-ts-webserver::aws:ec2/instance:Instance::web-server-www]
<{%reset%}><{%fg 9%}> [provider=urn:pulumi:dev::aws-ts-webserver::pulumi:providers:aws::default_3_38_1::57baf899-740a-486a-908e-43cf27cce182]
<{%reset%}><{%fg 9%}> ami : <{%reset%}><{%fg 9%}>"ami-0175af5baaf2bce8e"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> getPasswordData : <{%reset%}><{%fg 9%}>false<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> instanceType : <{%reset%}><{%fg 9%}>"t2.micro"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> sourceDestCheck : <{%reset%}><{%fg 9%}>true<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> tags : <{%reset%}><{%fg 9%}>{
<{%reset%}><{%fg 9%}> Name : <{%reset%}><{%fg 9%}>"web-server-www"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> }<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> userData : <{%reset%}><{%fg 9%}>"#!/bin/bash\necho \"Hello, World!\" > index.html\nnohup python -m SimpleHTTPServer 80 &"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> vpcSecurityGroupIds: <{%reset%}><{%fg 9%}>[
<{%reset%}><{%fg 9%}> [0]: <{%reset%}><{%fg 9%}>"sg-07498abcdbdf88f34"<{%reset%}><{%fg 9%}>
<{%reset%}><{%fg 9%}> ]<{%reset%}><{%fg 9%}>
<{%reset%}><{%reset%}><{%fg 13%}><{%bold%}>Resources:<{%reset%}>
<{%fg 13%}>+-1 replaced<{%reset%}>
2 unchanged

<{%fg 13%}><{%bold%}>Duration:<{%reset%}> 12s

0 comments on commit 21b9f29

Please sign in to comment.