Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support common prefix listing of diffs (uncommitted, commit view, compare view) #2051

Merged
merged 16 commits into from
Jun 7, 2021

Conversation

ozkatz
Copy link
Collaborator

@ozkatz ozkatz commented Jun 2, 2021

Addressing some of #2029 - When working with large diffs, paging across multiple data objects is not optimal.
This PR adds the ability (a toggle in the UI) to view diffs with a directory structure. Should make answering "which tables/partitions were modifed" much easier.

@ozkatz ozkatz added area/API Improvements or additions to the API area/UI Improvements or additions to UI labels Jun 2, 2021
@ozkatz ozkatz self-assigned this Jun 2, 2021
@lgtm-com
Copy link

lgtm-com bot commented Jun 2, 2021

This pull request introduces 1 alert when merging 15869ed into 50391bb - view on LGTM.com

new alerts:

  • 1 for Unused variable, import, function or class

@lgtm-com
Copy link

lgtm-com bot commented Jun 2, 2021

This pull request introduces 1 alert when merging a9752a4 into 50391bb - view on LGTM.com

new alerts:

  • 1 for Unused variable, import, function or class

Copy link
Contributor

@nopcoder nopcoder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! minor style comments

Comment on lines 1622 to 1626
if params.Delimiter == nil {
delimiter = ""
} else {
delimiter = *params.Delimiter
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep it simple

Suggested change
if params.Delimiter == nil {
delimiter = ""
} else {
delimiter = *params.Delimiter
}
if params.Delimiter != nil {
delimiter = *params.Delimiter
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment on lines 2184 to 2188
if params.Delimiter == nil {
delimiter = ""
} else {
delimiter = *params.Delimiter
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if params.Delimiter == nil {
delimiter = ""
} else {
delimiter = *params.Delimiter
}
if params.Delimiter != nil {
delimiter = *params.Delimiter
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

}

func GetStartPos(prefix, after, delimiter string) string {
// find the correct place to start iterating from based on delimiter, prefix, after
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convert to godoc for this func

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -50,7 +52,10 @@ export const ChangeEntryRow = ({ entry, showActions, onRevert }) => {
break;
}

const pathText = entry.path;
let pathText = entry.path;
if (pathText.indexOf(relativeTo) === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (pathText.indexOf(relativeTo) === 0) {
if (pathText.startsWith(relativeTo)) {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines +1031 to +1036
if delimiter != "" {
// common prefix logic goes here.
// for every path, after trimming "prefix", take the string upto-and-including the delimiter
// if the received path == the entire remainder, add that object as is
// if it's just a part of the name, add it as a "common prefix" entry -
// and skip to next record following all those starting with this prefix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider reduce the code block size inside this loop by splitting it to extractCommonPrefix, checking if there is a common prefix, and fall to the part where we append and check the limit)

Comment on lines +307 to +309
after: (!!after) ? after : "",
prefix: (!!prefix) ? prefix : "",
delimiter: (!!delimiter) ? delimiter : "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to convert to boolean when we evaluate an expression already

Suggested change
after: (!!after) ? after : "",
prefix: (!!prefix) ? prefix : "",
delimiter: (!!delimiter) ? delimiter : "",
after: after ? after : "",
prefix: prefix ? prefix : "",
delimiter: delimiter ? delimiter : "",

Comment on lines 297 to +299
after={(!!after) ? after : ""}
prefix={(!!prefix) ? prefix : ""}
delimiter={(!!delimiter) ? delimiter : ""}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
after={(!!after) ? after : ""}
prefix={(!!prefix) ? prefix : ""}
delimiter={(!!delimiter) ? delimiter : ""}
after={after ? after : ""}
prefix={prefix ? prefix : ""}
delimiter={delimiter ? delimiter : ""}

return {pathname: '/repositories/:repoId/objects', params, query};
};

export const URINavigator = ({ repo, reference, path, relativeTo = "", pathURLBuilder = buildPathURL }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const URINavigator = ({ repo, reference, path, relativeTo = "", pathURLBuilder = buildPathURL }) => {
export const URINavigator = ({ repo, reference, path, relativeTo="", pathURLBuilder=buildPathURL }) => {

Copy link
Contributor

@arielshaqed arielshaqed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note LGTM.com comments -- they are sometimes worthwhile, and in any case I'd rather keep the number of open comments down.

Thanks, it looks great and my comments are minor.

})
}
}

func TestCatalog_ListRepositories(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can/should we also add tests for listings with empty prefixes, i.e. paths such as a/b///c, a/b//c, a/b/c?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, added

Copy link
Contributor

@johnnyaug johnnyaug left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was fast


// discern between an empty delimiter and no delimiter being passed at all
// by default, go-swagger will use the default value ("/") even if we pass
// a delimiter param that is explicitly empty. This overrides this (wrong) behavior.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior is not wrong, adding allowEmptyValue: true to the parameter definition should solve this.

Copy link
Collaborator Author

@ozkatz ozkatz Jun 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this uncovered a weird behavior in our system. Once upon a time, there was no delimiter in the object listing API.
It worked as if there's always a delimiter that is "/". When we added a configuratble one, we tried being backwards compatible. IMO, this was probably a mistake. It worked like this:

  • if no delimiter is passed, assume delimiter = /
  • if delimiter is passed but is empty (i.e. ?delimiter=), assume no delimiter
  • if delimiter is passed and is any other value, use that.

while it retained the previous behavior it's a weird choice. it means that if I add a delimiter parameter to diff endpoints I can choose between:

  1. doing the same thing and now assume that no delimiter actually means / - this would now break existing diff clients.
  2. make the API inconsistent by having different defaults: for objects, no delimiter means /, for diff it means no delimiter.
  3. make the API consistent by fixing object listing (and breaking it): no delimiter means no delimiter.

I went with the painful option 3.

@@ -10,7 +10,7 @@ func transformDifferenceTypeToString(d catalog.DifferenceType) string {
return "added"
case catalog.DifferenceTypeRemoved:
return "removed"
case catalog.DifferenceTypeChanged:
case catalog.DifferenceTypeChanged, catalog.DifferenceTypeCommonPrefix:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So common prefixes are always shown as changed, regardless of what happened underneath? Worth adding a comment here.

Copy link
Collaborator Author

@ozkatz ozkatz Jun 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this diff type as it isn't a diff type.

@@ -188,7 +188,7 @@
}

.diff-changed {
background-color: #d1ecf1;
background-color: #ffeaa7;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

return ""
case delimiter != "" && after != "":
// if we have a delimiter and after is not empty, start at the next common prefix after "after"
return string(graveler.UpperBoundForPrefix([]byte(after)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear to me why the presence of a delimiter changes the return value of this function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with a normal after, you'd want to start the page with the next lexicographically sorted entry after after.
Once a delimiter is present, you'd want the next entry (or prefix) that doesn't start with after.

Example:

after = a/b/
without a delimiter, a/b/c would be a valid first entry
with a delimiter /, a/b/c is wrong, as it is already folded into a/b/ - the first valid entry would be the next entry not starting with a/b/

func listDiffHelper(it EntryDiffIterator, limit int, after string) (Differences, bool, error) {
const commonPrefixSplitParts = 2

func listDiffHelper(it EntryDiffIterator, prefix, delimiter string, limit int, after string) (Differences, bool, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's time to add unit tests for catalog diff/compare.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppose the diff contains two entries b and c.
Suppose after=a, and prefix=c.

The returned diff should be c. I think this function will return an empty diff.
(This may be an abuse of this function, but still as a unit it should return valid results)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - but this is logic that should be in GetStartPos - as in this case the start pos should be "c" and not "a". I also added a test case for it.

<Card.Header>
<span className="float-left">
{(delimiter !== "") && (
<URINavigator path={prefix} reference={reference} repo={repo} relativeTo={`${reference.id} workspace`} pathURLBuilder={(params, query) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"workspace" is a term we didn't publicly mention yet - I think best to avoid it for now. Also here it appears in the same font as the path - which is confusing.

Also the breadcrumb links do not work for me

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do use it publicly..

<URINavigator
path={prefix}
reference={reference}
relativeTo={`${reference.id}...${compareReference.id}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: trim if reference is a commit hash, to conform with a single commit view

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

return {
pathname: '/repositories/:repoId/commits/:commitId',
params: {repoId: repo.id, commitId: commit.id},
query: {delimiter: "/", prefix: query.path}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the right place to hardcode a /?

</span>
<span className="float-right">
<Form>
<Form.Switch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: extract the toggle switch to a component

@ozkatz ozkatz requested review from johnnyaug and arielshaqed June 6, 2021 16:45
@ozkatz
Copy link
Collaborator Author

ozkatz commented Jun 6, 2021

@nopcoder @johnnyaug @arielshaqed - please notice that I broke backwards compatibility for the object listing API. If you have a better idea I'd love to not do it.

@lgtm-com
Copy link

lgtm-com bot commented Jun 6, 2021

This pull request introduces 1 alert when merging 04fb060 into d9ae3b9 - view on LGTM.com

new alerts:

  • 1 for Unused variable, import, function or class

Copy link
Contributor

@arielshaqed arielshaqed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. About b/c: as long as existing deployed clients are OK (I think we have just the lakectl command) I see no issues with breaking b/c on some weird-ish edge cases.
  2. Note new LGTM.com complaint.
  3. Two minor comments.
  4. ???
  5. THANKS (& PROFIT)!!!

api/swagger.yml Outdated
Comment on lines 308 to 310
size_bytes:
type: integer
format: int64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear what is the "size" of a diff. Can you add a description please?

@@ -935,8 +935,9 @@ func TestController_ObjectsListObjectsHandler(t *testing.T) {
}

t.Run("get object list", func(t *testing.T) {
pfx := api.PaginationPrefix("foo/")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pfx := api.PaginationPrefix("foo/")
prefix := api.PaginationPrefix("foo/")

Why abbreviate?

Copy link
Contributor

@johnnyaug johnnyaug left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.
In the UI, I still suggest dropping the word "workspace" from the path.

@ozkatz ozkatz merged commit b66a22e into master Jun 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/API Improvements or additions to the API area/UI Improvements or additions to UI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants