Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
037f5eef
Commit
037f5eef
authored
Oct 08, 2021
by
Jacques Erasmus
Committed by
Peter Hegman
Oct 08, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Lazy load commit data
Lazy loads commit data using itersectionObserver
parent
026d483d
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
408 additions
and
38 deletions
+408
-38
app/assets/javascripts/repository/commits_service.js
app/assets/javascripts/repository/commits_service.js
+65
-0
app/assets/javascripts/repository/components/table/index.vue
app/assets/javascripts/repository/components/table/index.vue
+38
-1
app/assets/javascripts/repository/components/table/row.vue
app/assets/javascripts/repository/components/table/row.vue
+46
-9
app/assets/javascripts/repository/components/tree_content.vue
...assets/javascripts/repository/components/tree_content.vue
+21
-1
app/assets/javascripts/repository/constants.js
app/assets/javascripts/repository/constants.js
+4
-0
app/controllers/projects/tree_controller.rb
app/controllers/projects/tree_controller.rb
+1
-0
app/controllers/projects_controller.rb
app/controllers/projects_controller.rb
+1
-0
config/feature_flags/development/lazy_load_commits.yml
config/feature_flags/development/lazy_load_commits.yml
+8
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/repository/commits_service_spec.js
spec/frontend/repository/commits_service_spec.js
+84
-0
spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
...epository/components/table/__snapshots__/row_spec.js.snap
+54
-21
spec/frontend/repository/components/table/index_spec.js
spec/frontend/repository/components/table/index_spec.js
+32
-1
spec/frontend/repository/components/table/row_spec.js
spec/frontend/repository/components/table/row_spec.js
+30
-4
spec/frontend/repository/components/tree_content_spec.js
spec/frontend/repository/components/tree_content_spec.js
+21
-1
No files found.
app/assets/javascripts/repository/commits_service.js
0 → 100644
View file @
037f5eef
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
import
{
normalizeData
}
from
'
ee_else_ce/repository/utils/commit
'
;
import
createFlash
from
'
~/flash
'
;
import
{
COMMIT_BATCH_SIZE
,
I18N_COMMIT_DATA_FETCH_ERROR
}
from
'
./constants
'
;
let
requestedOffsets
=
[];
let
fetchedBatches
=
[];
export
const
isRequested
=
(
offset
)
=>
requestedOffsets
.
includes
(
offset
);
export
const
resetRequestedCommits
=
()
=>
{
requestedOffsets
=
[];
fetchedBatches
=
[];
};
const
addRequestedOffset
=
(
offset
)
=>
{
if
(
isRequested
(
offset
)
||
offset
<
0
)
{
return
;
}
requestedOffsets
.
push
(
offset
);
};
const
removeLeadingSlash
=
(
path
)
=>
path
.
replace
(
/^
\/
/
,
''
);
const
fetchData
=
(
projectPath
,
path
,
ref
,
offset
)
=>
{
if
(
fetchedBatches
.
includes
(
offset
)
||
offset
<
0
)
{
return
[];
}
fetchedBatches
.
push
(
offset
);
const
url
=
joinPaths
(
gon
.
relative_url_root
||
'
/
'
,
projectPath
,
'
/-/refs/
'
,
ref
,
'
/logs_tree/
'
,
encodeURIComponent
(
removeLeadingSlash
(
path
)),
);
return
axios
.
get
(
url
,
{
params
:
{
format
:
'
json
'
,
offset
}
})
.
then
(({
data
})
=>
normalizeData
(
data
,
path
))
.
catch
(()
=>
createFlash
({
message
:
I18N_COMMIT_DATA_FETCH_ERROR
}));
};
export
const
loadCommits
=
async
(
projectPath
,
path
,
ref
,
offset
)
=>
{
if
(
isRequested
(
offset
))
{
return
[];
}
// We fetch in batches of 25, so this ensures we don't refetch
Array
.
from
(
Array
(
COMMIT_BATCH_SIZE
)).
forEach
((
_
,
i
)
=>
{
addRequestedOffset
(
offset
-
i
);
addRequestedOffset
(
offset
+
i
);
});
// Since a user could scroll either up or down, we want to support lazy loading in both directions
const
commitsBatchUp
=
await
fetchData
(
projectPath
,
path
,
ref
,
offset
-
COMMIT_BATCH_SIZE
);
const
commitsBatchDown
=
await
fetchData
(
projectPath
,
path
,
ref
,
offset
);
return
commitsBatchUp
.
concat
(
commitsBatchDown
);
};
app/assets/javascripts/repository/components/table/index.vue
View file @
037f5eef
<
script
>
<
script
>
import
{
GlDeprecatedSkeletonLoading
as
GlSkeletonLoading
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
GlDeprecatedSkeletonLoading
as
GlSkeletonLoading
,
GlButton
}
from
'
@gitlab/ui
'
;
import
glFeatureFlagMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
{
sprintf
,
__
}
from
'
../../../locale
'
;
import
{
sprintf
,
__
}
from
'
../../../locale
'
;
import
getRefMixin
from
'
../../mixins/get_ref
'
;
import
getRefMixin
from
'
../../mixins/get_ref
'
;
import
projectPathQuery
from
'
../../queries/project_path.query.graphql
'
;
import
projectPathQuery
from
'
../../queries/project_path.query.graphql
'
;
...
@@ -15,13 +16,18 @@ export default {
...
@@ -15,13 +16,18 @@ export default {
ParentRow
,
ParentRow
,
GlButton
,
GlButton
,
},
},
mixins
:
[
getRefMixin
],
mixins
:
[
getRefMixin
,
glFeatureFlagMixin
()
],
apollo
:
{
apollo
:
{
projectPath
:
{
projectPath
:
{
query
:
projectPathQuery
,
query
:
projectPathQuery
,
},
},
},
},
props
:
{
props
:
{
commits
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
path
:
{
path
:
{
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
...
@@ -48,6 +54,7 @@ export default {
...
@@ -48,6 +54,7 @@ export default {
data
()
{
data
()
{
return
{
return
{
projectPath
:
''
,
projectPath
:
''
,
rowNumbers
:
{},
};
};
},
},
computed
:
{
computed
:
{
...
@@ -73,10 +80,37 @@ export default {
...
@@ -73,10 +80,37 @@ export default {
return
[
''
,
'
/
'
].
indexOf
(
this
.
path
)
===
-
1
;
return
[
''
,
'
/
'
].
indexOf
(
this
.
path
)
===
-
1
;
},
},
},
},
watch
:
{
$route
:
function
routeChange
()
{
this
.
$options
.
totalRowsLoaded
=
-
1
;
},
},
totalRowsLoaded
:
-
1
,
methods
:
{
methods
:
{
showMore
()
{
showMore
()
{
this
.
$emit
(
'
showMore
'
);
this
.
$emit
(
'
showMore
'
);
},
},
generateRowNumber
(
id
)
{
if
(
!
this
.
glFeatures
.
lazyLoadCommits
)
{
return
0
;
}
if
(
!
this
.
rowNumbers
[
id
]
&&
this
.
rowNumbers
[
id
]
!==
0
)
{
this
.
$options
.
totalRowsLoaded
+=
1
;
this
.
rowNumbers
[
id
]
=
this
.
$options
.
totalRowsLoaded
;
}
return
this
.
rowNumbers
[
id
];
},
getCommit
(
fileName
,
type
)
{
if
(
!
this
.
glFeatures
.
lazyLoadCommits
)
{
return
{};
}
return
this
.
commits
.
find
(
(
commitEntry
)
=>
commitEntry
.
fileName
===
fileName
&&
commitEntry
.
type
===
type
,
);
},
},
},
};
};
</
script
>
</
script
>
...
@@ -116,6 +150,9 @@ export default {
...
@@ -116,6 +150,9 @@ export default {
:lfs-oid="entry.lfsOid"
:lfs-oid="entry.lfsOid"
:loading-path="loadingPath"
:loading-path="loadingPath"
:total-entries="totalEntries"
:total-entries="totalEntries"
:row-number="generateRowNumber(entry.id)"
:commit-info="getCommit(entry.name, entry.type)"
v-on="$listeners"
/>
/>
</
template
>
</
template
>
<
template
v-if=
"isLoading"
>
<
template
v-if=
"isLoading"
>
...
...
app/assets/javascripts/repository/components/table/row.vue
View file @
037f5eef
...
@@ -8,6 +8,7 @@ import {
...
@@ -8,6 +8,7 @@ import {
GlIcon
,
GlIcon
,
GlHoverLoadDirective
,
GlHoverLoadDirective
,
GlSafeHtmlDirective
,
GlSafeHtmlDirective
,
GlIntersectionObserver
,
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
{
escapeRegExp
}
from
'
lodash
'
;
import
{
escapeRegExp
}
from
'
lodash
'
;
import
filesQuery
from
'
shared_queries/repository/files.query.graphql
'
;
import
filesQuery
from
'
shared_queries/repository/files.query.graphql
'
;
...
@@ -30,6 +31,7 @@ export default {
...
@@ -30,6 +31,7 @@ export default {
GlIcon
,
GlIcon
,
TimeagoTooltip
,
TimeagoTooltip
,
FileIcon
,
FileIcon
,
GlIntersectionObserver
,
},
},
directives
:
{
directives
:
{
GlTooltip
:
GlTooltipDirective
,
GlTooltip
:
GlTooltipDirective
,
...
@@ -48,10 +50,23 @@ export default {
...
@@ -48,10 +50,23 @@ export default {
maxOffset
:
this
.
totalEntries
,
maxOffset
:
this
.
totalEntries
,
};
};
},
},
skip
()
{
return
this
.
glFeatures
.
lazyLoadCommits
;
},
},
},
},
},
mixins
:
[
getRefMixin
,
glFeatureFlagMixin
()],
mixins
:
[
getRefMixin
,
glFeatureFlagMixin
()],
props
:
{
props
:
{
commitInfo
:
{
type
:
Object
,
required
:
false
,
default
:
null
,
},
rowNumber
:
{
type
:
Number
,
required
:
false
,
default
:
null
,
},
totalEntries
:
{
totalEntries
:
{
type
:
Number
,
type
:
Number
,
required
:
true
,
required
:
true
,
...
@@ -113,9 +128,13 @@ export default {
...
@@ -113,9 +128,13 @@ export default {
data
()
{
data
()
{
return
{
return
{
commit
:
null
,
commit
:
null
,
hasRowAppeared
:
false
,
};
};
},
},
computed
:
{
computed
:
{
commitData
()
{
return
this
.
glFeatures
.
lazyLoadCommits
?
this
.
commitInfo
:
this
.
commit
;
},
refactorBlobViewerEnabled
()
{
refactorBlobViewerEnabled
()
{
return
this
.
glFeatures
.
refactorBlobViewer
;
return
this
.
glFeatures
.
refactorBlobViewer
;
},
},
...
@@ -148,7 +167,10 @@ export default {
...
@@ -148,7 +167,10 @@ export default {
return
this
.
sha
.
slice
(
0
,
8
);
return
this
.
sha
.
slice
(
0
,
8
);
},
},
hasLockLabel
()
{
hasLockLabel
()
{
return
this
.
commit
&&
this
.
commit
.
lockLabel
;
return
this
.
commitData
&&
this
.
commitData
.
lockLabel
;
},
showSkeletonLoader
()
{
return
!
this
.
commitData
&&
this
.
hasRowAppeared
;
},
},
},
},
methods
:
{
methods
:
{
...
@@ -179,6 +201,19 @@ export default {
...
@@ -179,6 +201,19 @@ export default {
apolloQuery
(
query
,
variables
)
{
apolloQuery
(
query
,
variables
)
{
this
.
$apollo
.
query
({
query
,
variables
});
this
.
$apollo
.
query
({
query
,
variables
});
},
},
rowAppeared
()
{
this
.
hasRowAppeared
=
true
;
if
(
this
.
glFeatures
.
lazyLoadCommits
)
{
this
.
$emit
(
'
row-appear
'
,
{
rowNumber
:
this
.
rowNumber
,
hasCommit
:
Boolean
(
this
.
commitInfo
),
});
}
},
rowDisappeared
()
{
this
.
hasRowAppeared
=
false
;
},
},
},
safeHtmlConfig
:
{
ADD_TAGS
:
[
'
gl-emoji
'
]
},
safeHtmlConfig
:
{
ADD_TAGS
:
[
'
gl-emoji
'
]
},
};
};
...
@@ -222,7 +257,7 @@ export default {
...
@@ -222,7 +257,7 @@ export default {
<gl-icon
<gl-icon
v-if=
"hasLockLabel"
v-if=
"hasLockLabel"
v-gl-tooltip
v-gl-tooltip
:title=
"commit.lockLabel"
:title=
"commit
Data
.lockLabel"
name=
"lock"
name=
"lock"
:size=
"12"
:size=
"12"
class=
"ml-1"
class=
"ml-1"
...
@@ -230,17 +265,19 @@ export default {
...
@@ -230,17 +265,19 @@ export default {
</td>
</td>
<td
class=
"d-none d-sm-table-cell tree-commit cursor-default"
>
<td
class=
"d-none d-sm-table-cell tree-commit cursor-default"
>
<gl-link
<gl-link
v-if=
"commit"
v-if=
"commit
Data
"
v-safe-html:
[$
options.safeHtmlConfig]=
"commit.titleHtml"
v-safe-html:
[$
options.safeHtmlConfig]=
"commit
Data
.titleHtml"
:href=
"commit.commitPath"
:href=
"commit
Data
.commitPath"
:title=
"commit.message"
:title=
"commit
Data
.message"
class=
"str-truncated-100 tree-commit-link"
class=
"str-truncated-100 tree-commit-link"
/>
/>
<gl-skeleton-loading
v-else
:lines=
"1"
class=
"h-auto"
/>
<gl-intersection-observer
@
appear=
"rowAppeared"
@
disappear=
"rowDisappeared"
>
<gl-skeleton-loading
v-if=
"showSkeletonLoader"
:lines=
"1"
class=
"h-auto"
/>
</gl-intersection-observer>
</td>
</td>
<td
class=
"tree-time-ago text-right cursor-default"
>
<td
class=
"tree-time-ago text-right cursor-default"
>
<timeago-tooltip
v-if=
"commit
"
:time=
"commit
.committedDate"
/>
<timeago-tooltip
v-if=
"commit
Data"
:time=
"commitData
.committedDate"
/>
<gl-skeleton-loading
v-
else
:lines=
"1"
class=
"ml-auto h-auto w-50"
/>
<gl-skeleton-loading
v-
if=
"showSkeletonLoader"
:lines=
"1"
class=
"ml-auto h-auto w-50"
/>
</td>
</td>
</tr>
</tr>
</template>
</template>
app/assets/javascripts/repository/components/tree_content.vue
View file @
037f5eef
...
@@ -8,6 +8,7 @@ import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT } from '../co
...
@@ -8,6 +8,7 @@ import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT } from '../co
import
getRefMixin
from
'
../mixins/get_ref
'
;
import
getRefMixin
from
'
../mixins/get_ref
'
;
import
projectPathQuery
from
'
../queries/project_path.query.graphql
'
;
import
projectPathQuery
from
'
../queries/project_path.query.graphql
'
;
import
{
readmeFile
}
from
'
../utils/readme
'
;
import
{
readmeFile
}
from
'
../utils/readme
'
;
import
{
loadCommits
,
isRequested
,
resetRequestedCommits
}
from
'
../commits_service
'
;
import
FilePreview
from
'
./preview/index.vue
'
;
import
FilePreview
from
'
./preview/index.vue
'
;
import
FileTable
from
'
./table/index.vue
'
;
import
FileTable
from
'
./table/index.vue
'
;
...
@@ -36,6 +37,7 @@ export default {
...
@@ -36,6 +37,7 @@ export default {
},
},
data
()
{
data
()
{
return
{
return
{
commits
:
[],
projectPath
:
''
,
projectPath
:
''
,
nextPageCursor
:
''
,
nextPageCursor
:
''
,
pagesLoaded
:
1
,
pagesLoaded
:
1
,
...
@@ -81,12 +83,16 @@ export default {
...
@@ -81,12 +83,16 @@ export default {
this
.
entries
.
submodules
=
[];
this
.
entries
.
submodules
=
[];
this
.
entries
.
blobs
=
[];
this
.
entries
.
blobs
=
[];
this
.
nextPageCursor
=
''
;
this
.
nextPageCursor
=
''
;
resetRequestedCommits
();
this
.
fetchFiles
();
this
.
fetchFiles
();
},
},
},
},
mounted
()
{
mounted
()
{
// We need to wait for `ref` and `projectPath` to be set
// We need to wait for `ref` and `projectPath` to be set
this
.
$nextTick
(()
=>
this
.
fetchFiles
());
this
.
$nextTick
(()
=>
{
resetRequestedCommits
();
this
.
fetchFiles
();
});
},
},
methods
:
{
methods
:
{
fetchFiles
()
{
fetchFiles
()
{
...
@@ -152,6 +158,18 @@ export default {
...
@@ -152,6 +158,18 @@ export default {
.
concat
(
data
.
trees
.
pageInfo
,
data
.
submodules
.
pageInfo
,
data
.
blobs
.
pageInfo
)
.
concat
(
data
.
trees
.
pageInfo
,
data
.
submodules
.
pageInfo
,
data
.
blobs
.
pageInfo
)
.
find
(({
hasNextPage
})
=>
hasNextPage
);
.
find
(({
hasNextPage
})
=>
hasNextPage
);
},
},
loadCommitData
({
rowNumber
=
0
,
hasCommit
}
=
{})
{
if
(
!
this
.
glFeatures
.
lazyLoadCommits
||
hasCommit
||
isRequested
(
rowNumber
))
{
return
;
}
loadCommits
(
this
.
projectPath
,
this
.
path
,
this
.
ref
,
rowNumber
)
.
then
(
this
.
setCommitData
)
.
catch
(()
=>
{});
},
setCommitData
(
data
)
{
this
.
commits
=
this
.
commits
.
concat
(
data
);
},
handleShowMore
()
{
handleShowMore
()
{
this
.
clickedShowMore
=
true
;
this
.
clickedShowMore
=
true
;
this
.
pagesLoaded
+=
1
;
this
.
pagesLoaded
+=
1
;
...
@@ -169,7 +187,9 @@ export default {
...
@@ -169,7 +187,9 @@ export default {
:is-loading=
"isLoadingFiles"
:is-loading=
"isLoadingFiles"
:loading-path=
"loadingPath"
:loading-path=
"loadingPath"
:has-more=
"hasShowMore"
:has-more=
"hasShowMore"
:commits=
"commits"
@
showMore=
"handleShowMore"
@
showMore=
"handleShowMore"
@
row-appear=
"loadCommitData"
/>
/>
<file-preview
v-if=
"readme"
:blob=
"readme"
/>
<file-preview
v-if=
"readme"
:blob=
"readme"
/>
</div>
</div>
...
...
app/assets/javascripts/repository/constants.js
View file @
037f5eef
...
@@ -4,6 +4,8 @@ export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
...
@@ -4,6 +4,8 @@ export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
export
const
TREE_PAGE_SIZE
=
100
;
// the amount of items to be fetched per (batch) request
export
const
TREE_PAGE_SIZE
=
100
;
// the amount of items to be fetched per (batch) request
export
const
TREE_INITIAL_FETCH_COUNT
=
TREE_PAGE_LIMIT
/
TREE_PAGE_SIZE
;
// the amount of (batch) requests to make
export
const
TREE_INITIAL_FETCH_COUNT
=
TREE_PAGE_LIMIT
/
TREE_PAGE_SIZE
;
// the amount of (batch) requests to make
export
const
COMMIT_BATCH_SIZE
=
25
;
// we request commit data in batches of 25
export
const
SECONDARY_OPTIONS_TEXT
=
__
(
'
Cancel
'
);
export
const
SECONDARY_OPTIONS_TEXT
=
__
(
'
Cancel
'
);
export
const
COMMIT_LABEL
=
__
(
'
Commit message
'
);
export
const
COMMIT_LABEL
=
__
(
'
Commit message
'
);
export
const
TARGET_BRANCH_LABEL
=
__
(
'
Target branch
'
);
export
const
TARGET_BRANCH_LABEL
=
__
(
'
Target branch
'
);
...
@@ -13,3 +15,5 @@ export const COMMIT_MESSAGE_SUBJECT_MAX_LENGTH = 52;
...
@@ -13,3 +15,5 @@ export const COMMIT_MESSAGE_SUBJECT_MAX_LENGTH = 52;
export
const
COMMIT_MESSAGE_BODY_MAX_LENGTH
=
72
;
export
const
COMMIT_MESSAGE_BODY_MAX_LENGTH
=
72
;
export
const
LIMITED_CONTAINER_WIDTH_CLASS
=
'
limit-container-width
'
;
export
const
LIMITED_CONTAINER_WIDTH_CLASS
=
'
limit-container-width
'
;
export
const
I18N_COMMIT_DATA_FETCH_ERROR
=
__
(
'
An error occurred while fetching commit data.
'
);
app/controllers/projects/tree_controller.rb
View file @
037f5eef
...
@@ -16,6 +16,7 @@ class Projects::TreeController < Projects::ApplicationController
...
@@ -16,6 +16,7 @@ class Projects::TreeController < Projects::ApplicationController
before_action
:authorize_edit_tree!
,
only:
[
:create_dir
]
before_action
:authorize_edit_tree!
,
only:
[
:create_dir
]
before_action
do
before_action
do
push_frontend_feature_flag
(
:lazy_load_commits
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:paginated_tree_graphql_query
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:paginated_tree_graphql_query
,
@project
,
default_enabled: :yaml
)
end
end
...
...
app/controllers/projects_controller.rb
View file @
037f5eef
...
@@ -33,6 +33,7 @@ class ProjectsController < Projects::ApplicationController
...
@@ -33,6 +33,7 @@ class ProjectsController < Projects::ApplicationController
before_action
:export_rate_limit
,
only:
[
:export
,
:download_export
,
:generate_new_export
]
before_action
:export_rate_limit
,
only:
[
:export
,
:download_export
,
:generate_new_export
]
before_action
do
before_action
do
push_frontend_feature_flag
(
:lazy_load_commits
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:refactor_blob_viewer
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:refactor_blob_viewer
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:refactor_text_viewer
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:refactor_text_viewer
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:increase_page_size_exponentially
,
@project
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:increase_page_size_exponentially
,
@project
,
default_enabled: :yaml
)
...
...
config/feature_flags/development/lazy_load_commits.yml
0 → 100644
View file @
037f5eef
---
name
:
lazy_load_commits
introduced_by_url
:
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71633
rollout_issue_url
:
https://gitlab.com/gitlab-org/gitlab/-/issues/342497
milestone
:
'
14.4'
type
:
development
group
:
group::source code
default_enabled
:
false
locale/gitlab.pot
View file @
037f5eef
...
@@ -3608,6 +3608,9 @@ msgstr ""
...
@@ -3608,6 +3608,9 @@ msgstr ""
msgid "An error occurred while fetching codequality mr diff reports."
msgid "An error occurred while fetching codequality mr diff reports."
msgstr ""
msgstr ""
msgid "An error occurred while fetching commit data."
msgstr ""
msgid "An error occurred while fetching commits. Retry the search."
msgid "An error occurred while fetching commits. Retry the search."
msgstr ""
msgstr ""
...
...
spec/frontend/repository/commits_service_spec.js
0 → 100644
View file @
037f5eef
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
loadCommits
,
isRequested
,
resetRequestedCommits
}
from
'
~/repository/commits_service
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
import
createFlash
from
'
~/flash
'
;
import
{
I18N_COMMIT_DATA_FETCH_ERROR
}
from
'
~/repository/constants
'
;
jest
.
mock
(
'
~/flash
'
);
describe
(
'
commits service
'
,
()
=>
{
let
mock
;
const
url
=
`
${
gon
.
relative_url_root
||
''
}
/my-project/-/refs/main/logs_tree/`
;
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
url
).
reply
(
httpStatus
.
OK
,
[],
{});
jest
.
spyOn
(
axios
,
'
get
'
);
});
afterEach
(()
=>
{
mock
.
restore
();
resetRequestedCommits
();
});
const
requestCommits
=
(
offset
,
project
=
'
my-project
'
,
path
=
''
,
ref
=
'
main
'
)
=>
loadCommits
(
project
,
path
,
ref
,
offset
);
it
(
'
calls axios get
'
,
async
()
=>
{
const
offset
=
10
;
const
project
=
'
my-project
'
;
const
path
=
'
my-path
'
;
const
ref
=
'
my-ref
'
;
const
testUrl
=
`
${
gon
.
relative_url_root
||
''
}
/
${
project
}
/-/refs/
${
ref
}
/logs_tree/
${
path
}
`
;
await
requestCommits
(
offset
,
project
,
path
,
ref
);
expect
(
axios
.
get
).
toHaveBeenCalledWith
(
testUrl
,
{
params
:
{
format
:
'
json
'
,
offset
}
});
});
it
(
'
encodes the path correctly
'
,
async
()
=>
{
await
requestCommits
(
1
,
'
some-project
'
,
'
with $peci@l ch@rs/
'
);
const
encodedUrl
=
'
/some-project/-/refs/main/logs_tree/with%20%24peci%40l%20ch%40rs%2F
'
;
expect
(
axios
.
get
).
toHaveBeenCalledWith
(
encodedUrl
,
expect
.
anything
());
});
it
(
'
calls axios get once per batch
'
,
async
()
=>
{
await
Promise
.
all
([
requestCommits
(
0
),
requestCommits
(
1
),
requestCommits
(
23
)]);
expect
(
axios
.
get
.
mock
.
calls
.
length
).
toEqual
(
1
);
});
it
(
'
calls axios get twice if an offset is larger than 25
'
,
async
()
=>
{
await
requestCommits
(
100
);
expect
(
axios
.
get
.
mock
.
calls
[
0
][
1
]).
toEqual
({
params
:
{
format
:
'
json
'
,
offset
:
75
}
});
expect
(
axios
.
get
.
mock
.
calls
[
1
][
1
]).
toEqual
({
params
:
{
format
:
'
json
'
,
offset
:
100
}
});
});
it
(
'
updates the list of requested offsets
'
,
async
()
=>
{
await
requestCommits
(
200
);
expect
(
isRequested
(
200
)).
toBe
(
true
);
});
it
(
'
resets the list of requested offsets
'
,
async
()
=>
{
await
requestCommits
(
300
);
resetRequestedCommits
();
expect
(
isRequested
(
300
)).
toBe
(
false
);
});
it
(
'
calls `createFlash` when the request fails
'
,
async
()
=>
{
const
invalidPath
=
'
/#@ some/path
'
;
const
invalidUrl
=
`
${
url
}${
invalidPath
}
`
;
mock
.
onGet
(
invalidUrl
).
replyOnce
(
httpStatus
.
INTERNAL_SERVER_ERROR
,
[],
{});
await
requestCommits
(
1
,
'
my-project
'
,
invalidPath
);
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
I18N_COMMIT_DATA_FETCH_ERROR
});
});
});
spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
View file @
037f5eef
...
@@ -31,25 +31,36 @@ exports[`Repository table row component renders a symlink table row 1`] = `
...
@@ -31,25 +31,36 @@ exports[`Repository table row component renders a symlink table row 1`] = `
<!---->
<!---->
<!---->
<gl-icon-stub
class="ml-1"
name="lock"
size="12"
title="Locked by Root"
/>
</td>
</td>
<td
<td
class="d-none d-sm-table-cell tree-commit cursor-default"
class="d-none d-sm-table-cell tree-commit cursor-default"
>
>
<gl-skeleton-loading-stub
<gl-link-stub
class="h-auto"
class="str-truncated-100 tree-commit-link"
lines="1"
/>
/>
<gl-intersection-observer-stub>
<!---->
</gl-intersection-observer-stub>
</td>
</td>
<td
<td
class="tree-time-ago text-right cursor-default"
class="tree-time-ago text-right cursor-default"
>
>
<gl-skeleton-loading-stub
<timeago-tooltip-stub
class="ml-auto h-auto w-50"
cssclass=""
lines="1"
time="2019-01-01"
tooltipplacement="top"
/>
/>
<!---->
</td>
</td>
</tr>
</tr>
`;
`;
...
@@ -85,25 +96,36 @@ exports[`Repository table row component renders table row 1`] = `
...
@@ -85,25 +96,36 @@ exports[`Repository table row component renders table row 1`] = `
<!---->
<!---->
<!---->
<gl-icon-stub
class="ml-1"
name="lock"
size="12"
title="Locked by Root"
/>
</td>
</td>
<td
<td
class="d-none d-sm-table-cell tree-commit cursor-default"
class="d-none d-sm-table-cell tree-commit cursor-default"
>
>
<gl-skeleton-loading-stub
<gl-link-stub
class="h-auto"
class="str-truncated-100 tree-commit-link"
lines="1"
/>
/>
<gl-intersection-observer-stub>
<!---->
</gl-intersection-observer-stub>
</td>
</td>
<td
<td
class="tree-time-ago text-right cursor-default"
class="tree-time-ago text-right cursor-default"
>
>
<gl-skeleton-loading-stub
<timeago-tooltip-stub
class="ml-auto h-auto w-50"
cssclass=""
lines="1"
time="2019-01-01"
tooltipplacement="top"
/>
/>
<!---->
</td>
</td>
</tr>
</tr>
`;
`;
...
@@ -139,25 +161,36 @@ exports[`Repository table row component renders table row for path with special
...
@@ -139,25 +161,36 @@ exports[`Repository table row component renders table row for path with special
<!---->
<!---->
<!---->
<gl-icon-stub
class="ml-1"
name="lock"
size="12"
title="Locked by Root"
/>
</td>
</td>
<td
<td
class="d-none d-sm-table-cell tree-commit cursor-default"
class="d-none d-sm-table-cell tree-commit cursor-default"
>
>
<gl-skeleton-loading-stub
<gl-link-stub
class="h-auto"
class="str-truncated-100 tree-commit-link"
lines="1"
/>
/>
<gl-intersection-observer-stub>
<!---->
</gl-intersection-observer-stub>
</td>
</td>
<td
<td
class="tree-time-ago text-right cursor-default"
class="tree-time-ago text-right cursor-default"
>
>
<gl-skeleton-loading-stub
<timeago-tooltip-stub
class="ml-auto h-auto w-50"
cssclass=""
lines="1"
time="2019-01-01"
tooltipplacement="top"
/>
/>
<!---->
</td>
</td>
</tr>
</tr>
`;
`;
spec/frontend/repository/components/table/index_spec.js
View file @
037f5eef
...
@@ -34,17 +34,45 @@ const MOCK_BLOBS = [
...
@@ -34,17 +34,45 @@ const MOCK_BLOBS = [
},
},
];
];
function
factory
({
path
,
isLoading
=
false
,
hasMore
=
true
,
entries
=
{}
})
{
const
MOCK_COMMITS
=
[
{
fileName
:
'
blob.md
'
,
type
:
'
blob
'
,
commit
:
{
message
:
'
Updated blob.md
'
,
},
},
{
fileName
:
'
blob2.md
'
,
type
:
'
blob
'
,
commit
:
{
message
:
'
Updated blob2.md
'
,
},
},
{
fileName
:
'
blob3.md
'
,
type
:
'
blob
'
,
commit
:
{
message
:
'
Updated blob3.md
'
,
},
},
];
function
factory
({
path
,
isLoading
=
false
,
hasMore
=
true
,
entries
=
{},
commits
=
[]
})
{
vm
=
shallowMount
(
Table
,
{
vm
=
shallowMount
(
Table
,
{
propsData
:
{
propsData
:
{
path
,
path
,
isLoading
,
isLoading
,
entries
,
entries
,
hasMore
,
hasMore
,
commits
,
},
},
mocks
:
{
mocks
:
{
$apollo
,
$apollo
,
},
},
provide
:
{
glFeatures
:
{
lazyLoadCommits
:
true
},
},
});
});
}
}
...
@@ -82,12 +110,15 @@ describe('Repository table component', () => {
...
@@ -82,12 +110,15 @@ describe('Repository table component', () => {
entries
:
{
entries
:
{
blobs
:
MOCK_BLOBS
,
blobs
:
MOCK_BLOBS
,
},
},
commits
:
MOCK_COMMITS
,
});
});
const
rows
=
vm
.
findAll
(
TableRow
);
const
rows
=
vm
.
findAll
(
TableRow
);
expect
(
rows
.
length
).
toEqual
(
3
);
expect
(
rows
.
length
).
toEqual
(
3
);
expect
(
rows
.
at
(
2
).
attributes
().
mode
).
toEqual
(
'
120000
'
);
expect
(
rows
.
at
(
2
).
attributes
().
mode
).
toEqual
(
'
120000
'
);
expect
(
rows
.
at
(
2
).
props
().
rowNumber
).
toBe
(
2
);
expect
(
rows
.
at
(
2
).
props
().
commitInfo
).
toEqual
(
MOCK_COMMITS
[
2
]);
});
});
describe
(
'
Show more button
'
,
()
=>
{
describe
(
'
Show more button
'
,
()
=>
{
...
...
spec/frontend/repository/components/table/row_spec.js
View file @
037f5eef
import
{
GlBadge
,
GlLink
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
GlBadge
,
GlLink
,
GlIcon
,
GlIntersectionObserver
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
RouterLinkStub
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
,
RouterLinkStub
}
from
'
@vue/test-utils
'
;
import
{
createMockDirective
,
getBinding
}
from
'
helpers/vue_mock_directive
'
;
import
{
createMockDirective
,
getBinding
}
from
'
helpers/vue_mock_directive
'
;
import
TableRow
from
'
~/repository/components/table/row.vue
'
;
import
TableRow
from
'
~/repository/components/table/row.vue
'
;
import
FileIcon
from
'
~/vue_shared/components/file_icon.vue
'
;
import
FileIcon
from
'
~/vue_shared/components/file_icon.vue
'
;
import
{
FILE_SYMLINK_MODE
}
from
'
~/vue_shared/constants
'
;
import
{
FILE_SYMLINK_MODE
}
from
'
~/vue_shared/constants
'
;
const
COMMIT_MOCK
=
{
lockLabel
:
'
Locked by Root
'
,
committedDate
:
'
2019-01-01
'
};
let
vm
;
let
vm
;
let
$router
;
let
$router
;
...
@@ -20,12 +22,14 @@ function factory(propsData = {}) {
...
@@ -20,12 +22,14 @@ function factory(propsData = {}) {
projectPath
:
'
gitlab-org/gitlab-ce
'
,
projectPath
:
'
gitlab-org/gitlab-ce
'
,
url
:
`https://test.com`
,
url
:
`https://test.com`
,
totalEntries
:
10
,
totalEntries
:
10
,
commitInfo
:
COMMIT_MOCK
,
rowNumber
:
123
,
},
},
directives
:
{
directives
:
{
GlHoverLoad
:
createMockDirective
(),
GlHoverLoad
:
createMockDirective
(),
},
},
provide
:
{
provide
:
{
glFeatures
:
{
refactorBlobViewer
:
true
},
glFeatures
:
{
refactorBlobViewer
:
true
,
lazyLoadCommits
:
true
},
},
},
mocks
:
{
mocks
:
{
$router
,
$router
,
...
@@ -40,6 +44,7 @@ function factory(propsData = {}) {
...
@@ -40,6 +44,7 @@ function factory(propsData = {}) {
describe
(
'
Repository table row component
'
,
()
=>
{
describe
(
'
Repository table row component
'
,
()
=>
{
const
findRouterLink
=
()
=>
vm
.
find
(
RouterLinkStub
);
const
findRouterLink
=
()
=>
vm
.
find
(
RouterLinkStub
);
const
findIntersectionObserver
=
()
=>
vm
.
findComponent
(
GlIntersectionObserver
);
afterEach
(()
=>
{
afterEach
(()
=>
{
vm
.
destroy
();
vm
.
destroy
();
...
@@ -226,8 +231,6 @@ describe('Repository table row component', () => {
...
@@ -226,8 +231,6 @@ describe('Repository table row component', () => {
currentPath
:
'
/
'
,
currentPath
:
'
/
'
,
});
});
vm
.
setData
({
commit
:
{
lockLabel
:
'
Locked by Root
'
,
committedDate
:
'
2019-01-01
'
}
});
return
vm
.
vm
.
$nextTick
().
then
(()
=>
{
return
vm
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
vm
.
find
(
GlIcon
).
exists
()).
toBe
(
true
);
expect
(
vm
.
find
(
GlIcon
).
exists
()).
toBe
(
true
);
expect
(
vm
.
find
(
GlIcon
).
props
(
'
name
'
)).
toBe
(
'
lock
'
);
expect
(
vm
.
find
(
GlIcon
).
props
(
'
name
'
)).
toBe
(
'
lock
'
);
...
@@ -246,4 +249,27 @@ describe('Repository table row component', () => {
...
@@ -246,4 +249,27 @@ describe('Repository table row component', () => {
expect
(
vm
.
find
(
FileIcon
).
props
(
'
loading
'
)).
toBe
(
true
);
expect
(
vm
.
find
(
FileIcon
).
props
(
'
loading
'
)).
toBe
(
true
);
});
});
describe
(
'
row visibility
'
,
()
=>
{
beforeEach
(()
=>
{
factory
({
id
:
'
1
'
,
sha
:
'
1
'
,
path
:
'
test
'
,
type
:
'
tree
'
,
currentPath
:
'
/
'
,
});
});
it
(
'
emits a `row-appear` event
'
,
()
=>
{
findIntersectionObserver
().
vm
.
$emit
(
'
appear
'
);
expect
(
vm
.
emitted
(
'
row-appear
'
)).
toEqual
([
[
{
hasCommit
:
true
,
rowNumber
:
123
,
},
],
]);
});
});
});
});
spec/frontend/repository/components/tree_content_spec.js
View file @
037f5eef
...
@@ -3,6 +3,13 @@ import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.g
...
@@ -3,6 +3,13 @@ import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.g
import
FilePreview
from
'
~/repository/components/preview/index.vue
'
;
import
FilePreview
from
'
~/repository/components/preview/index.vue
'
;
import
FileTable
from
'
~/repository/components/table/index.vue
'
;
import
FileTable
from
'
~/repository/components/table/index.vue
'
;
import
TreeContent
from
'
~/repository/components/tree_content.vue
'
;
import
TreeContent
from
'
~/repository/components/tree_content.vue
'
;
import
{
loadCommits
,
isRequested
,
resetRequestedCommits
}
from
'
~/repository/commits_service
'
;
jest
.
mock
(
'
~/repository/commits_service
'
,
()
=>
({
loadCommits
:
jest
.
fn
(()
=>
Promise
.
resolve
()),
isRequested
:
jest
.
fn
(),
resetRequestedCommits
:
jest
.
fn
(),
}));
let
vm
;
let
vm
;
let
$apollo
;
let
$apollo
;
...
@@ -23,6 +30,7 @@ function factory(path, data = () => ({})) {
...
@@ -23,6 +30,7 @@ function factory(path, data = () => ({})) {
glFeatures
:
{
glFeatures
:
{
increasePageSizeExponentially
:
true
,
increasePageSizeExponentially
:
true
,
paginatedTreeGraphqlQuery
:
true
,
paginatedTreeGraphqlQuery
:
true
,
lazyLoadCommits
:
true
,
},
},
},
},
});
});
...
@@ -45,7 +53,7 @@ describe('Repository table component', () => {
...
@@ -45,7 +53,7 @@ describe('Repository table component', () => {
expect
(
vm
.
find
(
FilePreview
).
exists
()).
toBe
(
true
);
expect
(
vm
.
find
(
FilePreview
).
exists
()).
toBe
(
true
);
});
});
it
(
'
trigger fetchFiles when mounted
'
,
async
()
=>
{
it
(
'
trigger fetchFiles
and resetRequestedCommits
when mounted
'
,
async
()
=>
{
factory
(
'
/
'
);
factory
(
'
/
'
);
jest
.
spyOn
(
vm
.
vm
,
'
fetchFiles
'
).
mockImplementation
(()
=>
{});
jest
.
spyOn
(
vm
.
vm
,
'
fetchFiles
'
).
mockImplementation
(()
=>
{});
...
@@ -53,6 +61,7 @@ describe('Repository table component', () => {
...
@@ -53,6 +61,7 @@ describe('Repository table component', () => {
await
vm
.
vm
.
$nextTick
();
await
vm
.
vm
.
$nextTick
();
expect
(
vm
.
vm
.
fetchFiles
).
toHaveBeenCalled
();
expect
(
vm
.
vm
.
fetchFiles
).
toHaveBeenCalled
();
expect
(
resetRequestedCommits
).
toHaveBeenCalled
();
});
});
describe
(
'
normalizeData
'
,
()
=>
{
describe
(
'
normalizeData
'
,
()
=>
{
...
@@ -180,4 +189,15 @@ describe('Repository table component', () => {
...
@@ -180,4 +189,15 @@ describe('Repository table component', () => {
});
});
});
});
});
});
it
(
'
loads commit data when row-appear event is emitted
'
,
()
=>
{
const
path
=
'
some/path
'
;
const
rowNumber
=
1
;
factory
(
path
);
findFileTable
().
vm
.
$emit
(
'
row-appear
'
,
{
hasCommit
:
false
,
rowNumber
});
expect
(
isRequested
).
toHaveBeenCalledWith
(
rowNumber
);
expect
(
loadCommits
).
toHaveBeenCalledWith
(
''
,
path
,
''
,
rowNumber
);
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment