diff --git a/README.md b/README.md
index 7d97efd..2570550 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ work in progress...
* ~~Persistent build and console output information~~
* ~~Project relations (blocks, triggers, etc)~~
* ~~Writes to stderr must not break the build~~
-* Mail and jabber notifications (with commits, current step and error)
+* ~~Mail and jabber notifications~~
* ~~Rename notification strategies according to statuses~~
* ~~Work with git~~
* ~~Build every commit, commit with tag, etc~~
@@ -33,12 +33,15 @@ work in progress...
* ~~git checkout before reset~~
* slow move out from build page (with lot of output) to main page - several sec
-* when long line appear console output row numbers not on the same line with
-content
+* ~~when long line appear console output row numbers not on the same line with
+content~~
* some "undefined" comments in scm changes
* projects list scroll
-* Error during send: TypeError: Cannot read property 'changes' of undefined
-* Builds loss
+* ~~Error during send: TypeError: Cannot read property 'changes' of undefined~~
+* ~~Builds loss~~
+* ~~error on git after change branch: fatal: ambiguous argument '18a8ea4..branch':
+unknown revision or path not in the working tree.~~
+* "Uncaught TypeError: Cannot read property 'name' of undefined" at item.js (jade)
## Feature requests
@@ -46,7 +49,7 @@ content
* ~~should write at the end of build console out that build is done (or error)~~
* ~~share workspace files at static~~
* "clear workspace" button
-* show more builds button (or infinity scroll) on start page
+* ~~show more builds button (or infinity scroll) on start page~~
* ~~hide console output by default (when go on completed build page you scroll
down to the output which could be very long)~~
* speed up build points animation at ff (maybe borrow something from animate.css?)
diff --git a/app.js b/app.js
index 1e5d1fa..050a588 100644
--- a/app.js
+++ b/app.js
@@ -32,7 +32,7 @@ var server = http.createServer(function(req, res) {
}
if (req.url.indexOf('/data.io.js') === -1) {
- if (/(js|css|fonts)/.test(req.url)) {
+ if (/(js|css|fonts|images)/.test(req.url)) {
staticServer.serve(req, res);
} else {
// serve index for all app pages
diff --git a/app/components/builds/item/index.jade b/app/components/builds/item/index.jade
index 3d1a049..506af6f 100644
--- a/app/components/builds/item/index.jade
+++ b/app/components/builds/item/index.jade
@@ -13,42 +13,61 @@ mixin statusText(build)
- var build = this.props.build;
-.build(class="")
- .build_content
- .build_status
- .status(class="status__#{build.status}")
- div.build_header
- if build.project
- span
- Scm(scm=build.project.scm.type)
+.builds_item(class="builds_item__#{build.status}")
+ .builds_inner
+ .row
+ .builds_header
+ if build.project
+ span
+ Scm(scm=build.project.scm.type)
+ |
+ Link(to="project", params={name: build.project.name})
+ span= build.project.name
|
- Link(to="project", params={name: build.project.name})
- span= build.project.name
- |
- if build.number
- span(style={fontSize: '15px', color: '#a6a6a6'}) build
- |
- if build.status !== 'queued'
- Link(to="build", params={id: build.id})
- span #
- span= build.number
- else
- span #
- span= build.number
+ if build.number
+ span(style={fontSize: '15px', color: '#a6a6a6'}) build
+ |
+ if build.status !== 'queued'
+ Link(to="build", params={id: build.id})
+ span #
+ span= build.number
+ else
+ span #
+ span= build.number
- if build.waitReason
- span (
- span= build.waitReason
- span , waiting)
+ if build.waitReason
+ span (
+ span= build.waitReason
+ span , waiting)
- if build.status === 'in-progress' && build.currentStep
- span (
- span= build.currentStep
- span )
+ if build.status === 'in-progress' && build.currentStep
+ span (
+ span= build.currentStep
+ span )
- div
+ .builds_controls
+ if build.completed
+ .builds_buttons
+ a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onRebuildProject(build.project.name))
+ i.fa.fa-fw.fa-repeat(title="Rebuild")
+ |
+ | Build again
+
+ if build.status === 'in-progress'
+ .builds_progress
+ if build.project.avgBuildDuration
+ Progress(build=build)
+
+ if build.status === 'queued'
+ .builds_buttons
+ a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onCancelBuild(build.id))
+ i.fa.fa-fw.fa-times(title="Cancel build")
+ |
+ | Cancel build
+
+ .builds_content
if build.endDate
- span.build_info
+ span.builds_info
i.fa.fa-fw.fa-clock-o
| finished
DateTime(value=build.endDate)
@@ -56,39 +75,18 @@ mixin statusText(build)
Duration(value=(build.endDate - build.startDate), withSuffix=true)
else
if build.startDate
- span.build_info
+ span.builds_info
i.fa.fa-fw.fa-clock-o
| started
DateTime(value=build.startDate)
else
- span.build_info
+ span.builds_info
i.fa.fa-fw.fa-clock-o
| queued
DateTime(value=build.createDate)
|
if build.scm
- span.build_info
+ span.builds_info
i.fa.fa-fw.fa-comment-o
|
span= utils.prune(build.scm.rev.comment, 40)
- |
-
- .build_controls
- if build.completed
- .build_controls_buttons
- a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onRebuildProject(build.project.name))
- i.fa.fa-fw.fa-repeat(title="Rebuild")
- |
- | Build again
- if build.status === 'in-progress'
- .build_controls_progress
- if build.project.avgBuildDuration
- Progress(build=build)
- if build.status === 'queued'
- .build_controls_buttons
- a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onCancelBuild(build.id))
- i.fa.fa-fw.fa-times(title="Cancel build")
- |
- | Cancel build
-
-
diff --git a/app/components/builds/list/index.jade b/app/components/builds/list/index.jade
index c08bbd9..db68abf 100644
--- a/app/components/builds/list/index.jade
+++ b/app/components/builds/list/index.jade
@@ -1,6 +1,15 @@
-.builds
- if !this.state.items.length
- p Build history is empty
- each build, index in this.state.items
- Item(build=build, key=build.id)
+- var itemsCount = this.state.items.length;
+if itemsCount
+ .builds.builds__timeline.builds__timeline-large(class="builds__timeline-#{itemsCount % 2 ? 'left' : 'right'}")
+ each build, index in this.state.items
+ Item(build=build, key=build.id)
+else
+ p Build history is empty
+
+if itemsCount && itemsCount % 20 === 0
+ .text-center
+ a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onShowMoreBuilds(this.props.projectName))
+ i.fa.fa-fw.fa-plus(title="Show more builds")
+ |
+ | Show more builds
diff --git a/app/components/builds/view/index.jade b/app/components/builds/view/index.jade
index 6076cec..f2380ed 100644
--- a/app/components/builds/view/index.jade
+++ b/app/components/builds/view/index.jade
@@ -15,29 +15,30 @@ mixin statusBadge(build)
if this.state.build
.col-sm-3.hidden-xs
BuildSidebar(projectName=this.state.build.project.name)
+
.col-sm-9
- h1
+ h1.page-header
.pull-right(style={fontSize: '22px'})
mixin statusBadge(this.state.build)
span Build #
span= this.state.build.number
- .text-muted(style={marginTop: '-10px'})
- | Initiated by
- - var initiator = this.state.build.initiator;
- if initiator.type === 'build'
- Link(to="project", params={name: initiator.project.name})
- span= initiator.project.name
- |
- | during the
- |
- Link(to="build", params={id: initiator.id})
- span build #
- span= initiator.number
- else
- span= initiator.type
+ .small.text-muted
+ | Initiated by
+ - var initiator = this.state.build.initiator;
+ if initiator.type === 'build'
+ Link(to="project", params={name: initiator.project.name})
+ span= initiator.project.name
+ |
+ | during the
+ |
+ Link(to="build", params={id: initiator.id})
+ span build #
+ span= initiator.number
+ else
+ span= initiator.type
- hr
+ //- hr
.build-view_info
if this.state.build.error
@@ -133,5 +134,5 @@ mixin statusBadge(build)
button.btn.btn-primary(onClick=this.toggleConsole)
i.fa.fa-fw.fa-refresh
|
- | Load console output...
+ | Show full console output
diff --git a/app/components/builds/view/sidebar/index.jade b/app/components/builds/view/sidebar/index.jade
index 86eeca3..e155b5b 100644
--- a/app/components/builds/view/sidebar/index.jade
+++ b/app/components/builds/view/sidebar/index.jade
@@ -1,17 +1,18 @@
-.builds(style={paddingTop: '20px'})
+.builds.builds__timeline.builds__timeline-small
each item in this.state.items
- .build.build__small(key=item.id)
- .build_content
- .build_status
- .status.status__small(class="status__#{item.status}")
- .build_header
- Link(to="build", params={id: item.id})
- span build #
- span= item.number
- .build_controls
- if item.status === 'in-progress'
- .build_controls_progress
- if item.project.avgBuildDuration
- Progress(build=item)
- if item.endDate
- DateTime(value=item.endDate)
+ .builds_item(key=item.id, class="builds_item__#{item.status}")
+ .builds_inner
+ .row
+ .builds_header
+ Link(to="build", params={id: item.id})
+ span build #
+ span= item.number
+
+ .builds_controls
+ if item.status === 'in-progress'
+ .builds_progress
+ if item.project.avgBuildDuration
+ Progress(build=item)
+
+ if !item.endDate
+ DateTime(value=item.endDate)
diff --git a/app/components/common/progress/index.jade b/app/components/common/progress/index.jade
index ef90fb2..cb7e1dc 100644
--- a/app/components/common/progress/index.jade
+++ b/app/components/common/progress/index.jade
@@ -1,2 +1,2 @@
.progress
- .progress-bar.progress-bar-success(style={width: this.state.percent + '%'})
+ .progress-bar.progress-bar-success.progress-bar-striped.active(style={width: this.state.percent + '%'})
diff --git a/app/components/dashboard/index.jade b/app/components/dashboard/index.jade
index f680a21..cfefea7 100644
--- a/app/components/dashboard/index.jade
+++ b/app/components/dashboard/index.jade
@@ -1,5 +1,4 @@
-.main-row
- .row
- .col-md-8.col-sm-12
- h2 Builds history
- BuildsList()
+div
+ h1.page-header Builds history
+
+ BuildsList()
diff --git a/app/components/projects/view/index.jade b/app/components/projects/view/index.jade
index 8e0c063..ab5d1ee 100644
--- a/app/components/projects/view/index.jade
+++ b/app/components/projects/view/index.jade
@@ -1,58 +1,56 @@
-.row
- .col-md-8
- h1.clearfix
- .pull-right
- button.btn.btn-sm.btn-primary.dropdown-toggle(
- data-toggle="dropdown",
- aria-expanded="false",
- disabled="true"
- )
- | target revision:
- span= this.state.project.scm ? this.state.project.scm.rev : ''
- |
- if this.state.project.name
- button.btn.btn-sm.btn-success(onClick=this.onBuildProject)
- i.fa.fa-fw.fa-play
- |
- span Build
- div
- Scm(scm=this.state.project.scm ? this.state.project.scm.type : '')
- span= this.state.project.name
-
- hr
-
- div.text-muted
- - var lastDoneBuild = this.state.project.lastDoneBuild;
- p Last successfully built:
- if lastDoneBuild
- DateTime(value=lastDoneBuild.endDate)
+div
+ h1.page-header.clearfix
+ .pull-right
+ button.btn.btn-sm.btn-primary.dropdown-toggle(
+ data-toggle="dropdown",
+ aria-expanded="false",
+ disabled="true"
+ )
+ | target revision:
+ span= this.state.project.scm ? this.state.project.scm.rev : ''
+ |
+ if this.state.project.name
+ button.btn.btn-sm.btn-success(onClick=this.onBuildProject)
+ i.fa.fa-fw.fa-play
|
- | (build #
- span= lastDoneBuild.number
- | )
- else
- | -
+ span Build
+ div
+ Scm(scm=this.state.project.scm ? this.state.project.scm.type : '')
+ span= this.state.project.name
- p Current successfully streak:
- if lastDoneBuild
- span= this.state.project.doneBuildsStreak
- else
- | -
+ div.text-muted
+ - var lastDoneBuild = this.state.project.lastDoneBuild;
+ p Last successfully built:
+ if lastDoneBuild
+ DateTime(value=lastDoneBuild.endDate)
+ |
+ | (build #
+ span= lastDoneBuild.number
+ | )
+ else
+ | -
- p Last build duration:
- if lastDoneBuild
- Duration(value=(lastDoneBuild.endDate - lastDoneBuild.startDate))
- else
- | -
+ p Current successfully streak:
+ if lastDoneBuild
+ span= this.state.project.doneBuildsStreak
+ else
+ | -
- p Average build duration:
- if this.state.project.avgBuildDuration
- Duration(value=this.state.project.avgBuildDuration)
- else
- | -
+ p Last build duration:
+ if lastDoneBuild
+ Duration(value=(lastDoneBuild.endDate - lastDoneBuild.startDate))
+ else
+ | -
- h2
- i.fa.fa-fw.fa-history
- span
- span Build history
- Builds(projectName=this.props.params.name)
+ p Average build duration:
+ if this.state.project.avgBuildDuration
+ Duration(value=this.state.project.avgBuildDuration)
+ else
+ | -
+
+ h2
+ i.fa.fa-fw.fa-history
+ span
+ span Build history
+
+ Builds(projectName=this.props.params.name)
diff --git a/app/components/terminal/index.jade b/app/components/terminal/index.jade
index 304ffa3..35cbd36 100644
--- a/app/components/terminal/index.jade
+++ b/app/components/terminal/index.jade
@@ -1,3 +1,3 @@
.terminal
pre.terminal_code
- .terminal_footer(style={height: '30px'})
+ .terminal_footer
diff --git a/app/styles/components/builds.less b/app/styles/components/builds.less
index 8c2e49b..5beb586 100644
--- a/app/styles/components/builds.less
+++ b/app/styles/components/builds.less
@@ -6,20 +6,48 @@
}
}
-.builds {
- padding: 0 15px;
-}
+@animation-duration: 1.5s;
-.build {
- .row();
- padding: 15px 0;
- margin-bottom: 3px;
- background: @well-bg;
+.builds {
+ &_item {
+ &:hover {
+ .builds {
+ &_buttons {
+ opacity: 1;
+ }
+ }
+ }
+ }
+
+ &_inner {
+ background: @well-bg;
+ padding: 15px;
+ }
+
+ &_header {
+ .make-xs-column(9);
+ }
+
+ &_controls {
+ .make-xs-column(3);
+ .text-right;
+ }
+
+ &_buttons {
+ transition: opacity 0.2s ease;
+ opacity: 0;
+ }
+
+ &_progress {
+ .progress {
+ height: 18px;
+ margin-bottom: 0;
+ }
+ }
&_status {
float: left;
- padding-top: 14px;
- margin-right: 13px;
+ margin-right: 8px;
}
&_info {
@@ -27,126 +55,301 @@
margin-right: 10px;
}
- &_controls {
- .make-md-column(3);
- .make-xs-column(3);
- .text-right;
+ &__timeline {
+ position: relative;
- &_buttons {
- margin-top: 8px;
- transition: opacity 0.2s ease;
- opacity: 0;
+ .builds {
+ &_inner {
+ border-left: 6px solid darken(@well-bg, 10%);
+ }
+
+ &_header {
+ margin-bottom: 6px;
+ font-size: 18px;
+ }
+
+ &_progress {
+ padding: 3px 0;
+ }
+
+ &_item {
+ margin: 4px 0;
+ position: relative;
+
+ &:after {
+ content: '';
+ position: absolute;
+ border-radius: 50%;
+ background: @well-bg;
+ left: 0;
+ z-index: 1;
+ }
+
+ &:before {
+ content: '';
+ position: absolute;
+ border: 11px solid transparent;
+ top: 25px;
+ }
+
+ &__in-progress {
+ &:after {
+ background: @brand-info;
+
+ -webkit-animation: pulsate @animation-duration ease-out;
+ -webkit-animation-iteration-count: infinite;
+ -moz-animation: pulsate @animation-duration ease-out;
+ -moz-animation-iteration-count: infinite;
+ -o-animation: pulsate @animation-duration ease-out;
+ -o-animation-iteration-count: infinite;
+ animation: pulsate @animation-duration ease-out;
+ animation-iteration-count: infinite;
+ }
+ }
+
+ &__done {
+ &:after {
+ background: @link-color;
+ }
+ }
+
+ &__error {
+ &:after {
+ background: @brand-danger;
+ }
+ }
+
+ &__queued {
+ &:after {
+ background: @brand-primary;
+ }
+ }
+ }
}
- &_progress {
- padding-top: 14px;
- .progress {
- height: 18px;
- margin-bottom: 0;
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ margin-left: -1px;
+ background: darken(@well-bg, 10%);
+ }
+
+ &-large {
+ .builds {
+ &_item {
+ padding-left: 40px;
+
+ &:after {
+ width: 24px;
+ height: 24px;
+ top: 25px;
+ }
+
+ &:before {
+ left: 20px;
+ border-right-color: darken(@well-bg, 10%);
+ top: 25px;
+ }
+ }
+ }
+
+ &:after {
+ left: 12px;
+ }
+ }
+
+ &-small {
+ .builds {
+ &_item {
+ padding-left: 30px;
+
+ &:after {
+ top: 16px;
+ height: 16px;
+ width: 16px;
+ }
+
+ &:before {
+ left: 10px;
+ border-right-color: darken(@well-bg, 10%);
+ top: 13px;
+ }
+ }
+
+ &_header {
+ font-size: 14px;
+ .make-xs-column(7);
+ margin-bottom: 0;
+ }
+
+ &_controls {
+ .make-xs-column(5);
+ font-size: 12px;
+ }
+
+ &_progress {
+ padding: 1px 0;
+ }
+ }
+
+ &:after {
+ left: 8px;
}
}
}
+}
- &_content {
- .make-md-column(9);
- .make-xs-column(9);
- }
+@media (min-width: @screen-sm-min) {
+ .builds {
+ &__timeline {
+ &-large {
+ .builds {
+ &_item {
+ padding-left: 0;
+ display: inline-block;
+ vertical-align: top;
+ width: 50%;
+ margin: 10px 0;
- &_header {
- margin-bottom: 6px;
- font-size: 18px;
- a {
- font-size: inherit;
- }
- }
+ &:after {
+ left: auto;
+ }
- &:hover {
- .build_controls_buttons {
- opacity: 1;
- }
- }
+ &:before {
+ left: auto;
+ border-right-color: transparent;
+ }
+ }
- &__small {
- .build_status {
- padding-top: 5px;
- }
- .build_header {
- font-size: 14px;
- }
- .build_content {
- .make-xs-column(7);
- }
- .build_controls {
- .make-xs-column(5);
- font-size: 12px;
- &_progress {
- padding-top: 4px;
- .progress {
- height: 12px;
+ &_inner {
+ border-left: 0;
+ }
+ }
+
+ &:after {
+ left: 50%;
+ }
+ }
+
+ &-left {
+ .builds {
+ &_item {
+ &:nth-child(odd) {
+ padding-right: 30px;
+
+ .builds {
+ &_inner {
+ border-right: 6px solid darken(@well-bg, 10%);
+ }
+ }
+
+ &:after {
+ right: -12px;
+ }
+
+ &:before {
+ right: 10px;
+ border-left-color: darken(@well-bg, 10%);
+ }
+ }
+
+ &:nth-child(even) {
+ padding-left: 30px;
+ top: 50px;
+
+ .builds {
+ &_inner {
+ border-left: 6px solid darken(@well-bg, 10%);
+ }
+ }
+
+ &:after {
+ left: -12px;
+ }
+
+ &:before {
+ left: 10px;
+ border-right-color: darken(@well-bg, 10%);
+ }
+ }
+ }
+ }
+ }
+
+ &-right {
+ .builds {
+ &_item {
+ &:first-child {
+ margin-left: 50%;
+ }
+
+ &:nth-child(even) {
+ padding-right: 30px;
+ top: -50px;
+
+ .builds {
+ &_inner {
+ border-right: 6px solid darken(@well-bg, 10%);
+ }
+ }
+
+ &:after {
+ right: -12px;
+ }
+
+ &:before {
+ right: 10px;
+ border-left-color: darken(@well-bg, 10%);
+ }
+ }
+
+ &:nth-child(odd) {
+ padding-left: 30px;
+
+ .builds {
+ &_inner {
+ border-left: 6px solid darken(@well-bg, 10%);
+ }
+ }
+
+ &:after {
+ left: -12px;
+ }
+
+ &:before {
+ left: 10px;
+ border-right-color: darken(@well-bg, 10%);
+ }
+ }
+ }
}
}
}
}
}
-@animation-duration: 1.5s;
-
-.status {
- width: 25px;
- height: 25px;
- border-radius: 50%;
-
- &__in-progress {
- background: @brand-info;
-
- -webkit-animation: pulsate @animation-duration ease-out;
- -webkit-animation-iteration-count: infinite;
- -moz-animation: pulsate @animation-duration ease-out;
- -moz-animation-iteration-count: infinite;
- -o-animation: pulsate @animation-duration ease-out;
- -o-animation-iteration-count: infinite;
- animation: pulsate @animation-duration ease-out;
- animation-iteration-count: infinite;
- }
- &__done {
- background: @link-color;
- }
- &__error {
- background: @brand-danger;
- }
- &__queued {
- background: @brand-primary;
- }
-
- &__small {
- width: 10px;
- height: 10px;
- }
-}
-
.pulsate-frames() {
.transform(@scaleX, @scaleY) {
- -webkit-transform: scale(@scaleX, @scaleY); opacity: 1;
- -moz-transform: scale(@scaleX, @scaleY); opacity: 1;
- -ms-transform: scale(@scaleX, @scaleY); opacity: 1;
- -o-transform: scale(@scaleX, @scaleY); opacity: 1;
- transform: scale(@scaleX, @scaleY); opacity: 1;
+ -webkit-transform: scale3d(@scaleX, @scaleY, 1);
+ -moz-transform: scale3d(@scaleX, @scaleY, 1);
+ -ms-transform: scale3d(@scaleX, @scaleY, 1);
+ -o-transform: scale3d(@scaleX, @scaleY, 1);
+ transform: scale3d(@scaleX, @scaleY, 1);
}
0% {
- .transform(1.0, 1.0);
- }
- 25% {
- opacity: 1.0;
+ .transform(1, 1);
}
+
50% {
.transform(0.75, 0.75);
}
- 50% {
- opacity: 1.0;
- }
+
100% {
- .transform(1.0, 1.0);
+ .transform(1, 1);
}
}
diff --git a/app/styles/components/layout.less b/app/styles/components/layout.less
index b1f864c..9337f27 100644
--- a/app/styles/components/layout.less
+++ b/app/styles/components/layout.less
@@ -1,7 +1,18 @@
body {
font-family: 'Open Sans', sans-serif;
+ padding-top: @navbar-height;
+ min-width: 320px;
}
.page-wrapper {
- margin-top: @navbar-height + 15px;
+ padding: 20px 0;
}
+
+.page-header {
+ margin-top: 0;
+ border-bottom-color: #d7d7d7;
+
+ .small {
+ font-size: @font-size-base;
+ }
+}
\ No newline at end of file
diff --git a/db.js b/db.js
index e2873fd..f0a0b88 100644
--- a/db.js
+++ b/db.js
@@ -210,7 +210,9 @@ nlevel.DocsSection.prototype._beforePut = function(docs, callback) {
// update createDate before put to provide latest date for last id
// it's rquired for correct generateIds function
_(docs).each(function(doc) {
- doc.createDate = Date.now();
+ if (!doc.id) {
+ doc.createDate = Date.now();
+ }
});
self.beforePut(docs, callback);
diff --git a/lib/distributor.js b/lib/distributor.js
index 42076b1..780ef0b 100644
--- a/lib/distributor.js
+++ b/lib/distributor.js
@@ -182,10 +182,21 @@ Distributor.prototype._onBuildComplete = function(build, callback) {
Distributor.prototype._updateBuild = function(build, changes, callback) {
var self = this;
callback = callback || _.noop;
- var isWithNumber = Boolean(build.number);
+ var isWithId = Boolean(build.id),
+ isWithNumber = Boolean(build.number);
Steppy(
function() {
+ if (build.id && changes.status && changes.status !== build.status) {
+ logger.log(
+ 'Build #%s (project "%s") change status: %s -> %s',
+ build.id,
+ build.project.name,
+ build.status,
+ changes.status
+ );
+ }
+
_(build).extend(changes);
// skip saving to db of unimportant data
@@ -196,6 +207,14 @@ Distributor.prototype._updateBuild = function(build, changes, callback) {
}
},
function() {
+ if (!isWithId && build.id) {
+ logger.log(
+ 'Build #%s (project "%s") %s',
+ build.id,
+ build.project.name,
+ build.status
+ );
+ }
// if number appear after save to db then add it to changes
// TODO: might be better to generate number right there (instead
diff --git a/lib/scm/git.js b/lib/scm/git.js
index b3ca2f4..948de55 100644
--- a/lib/scm/git.js
+++ b/lib/scm/git.js
@@ -133,12 +133,26 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) {
var self = this;
Steppy(
function() {
+ // get current rev to update on it after git log
+ self.getCurrent(this.slot());
+ },
+ function(err, currentRev) {
+ this.pass(currentRev);
+
+ // update to rev2 to prevent git error when switch to branch that
+ // doesn't exist locally: "unknown revision or path not in the
+ // working tree"
+ self.update(rev2, this.slot());
+ },
+ function(err, currentRev) {
+ this.pass(currentRev);
+
self.run({cmd: 'git', args: [
'log', rev1 ? rev1 + '..' + rev2 : rev2,
'--pretty=' + self._revTemplate
]}, this.slot());
},
- function(err, stdout) {
+ function(err, currentRev, stdout) {
// always skip last line - it's empty
var rows = stdout.split('\n').slice(0, -1);
@@ -146,7 +160,12 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) {
return self._parseRev(str);
});
+ this.pass(currentRev, changes);
+ },
+ function(err, currentRev, changes) {
this.pass(changes);
+
+ self.update(currentRev.id, this.slot());
},
callback
);
diff --git a/lib/scm/mercurial.js b/lib/scm/mercurial.js
index 2cf50c7..c69a4e2 100644
--- a/lib/scm/mercurial.js
+++ b/lib/scm/mercurial.js
@@ -15,13 +15,9 @@ inherits(Scm, ParentScm);
Scm.prototype.defaultRev = 'default';
-// use 2 invisible separators as fields separator
-Scm.prototype._fieldsSeparator = String.fromCharCode(2063);
-Scm.prototype._fieldsSeparator += Scm.prototype._fieldsSeparator;
-
-// use 2 vertical tabs as arrays separator
-Scm.prototype._arraysSeparator = String.fromCharCode(11);
-Scm.prototype._arraysSeparator += Scm.prototype._arraysSeparator;
+Scm.prototype._arraysSeparator = '\u2064' + '\u2064';
+Scm.prototype._fieldsSeparator = '\u2063' + '\u2063';
+Scm.prototype._linesSeparator = '\u2028' + '\u2028';
Scm.prototype._revTemplate = [
'{node|short}',
@@ -94,13 +90,15 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) {
function() {
self.run({cmd: 'hg', args: [
'log', '--rev', rev2 + ':' + rev1,
- '--template', self._revTemplate + '\n'
+ '--template', self._revTemplate + self._linesSeparator
]}, this.slot());
},
function(err, stdout) {
// always skip last line - it's empty and also skip first
// rev if we see range
- var rows = stdout.split('\n').slice(0, rev1 === rev2 ? -1 : -2);
+ var rows = stdout.split(self._linesSeparator).slice(
+ 0, rev1 === rev2 ? -1 : -2
+ );
var changes = _(rows).map(function(str) {
return self._parseRev(str);
diff --git a/package.json b/package.json
index bebfda1..ab92625 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nci",
- "version": "0.3.6",
+ "version": "0.4.1",
"description": "Continuous integration server written in node.js",
"bin": {
"nci": "bin/nci"
diff --git a/static/images/preloader.gif b/static/images/preloader.gif
new file mode 100644
index 0000000..a531cf9
Binary files /dev/null and b/static/images/preloader.gif differ
diff --git a/static/js/app/components/builds/list.js b/static/js/app/components/builds/list.js
new file mode 100644
index 0000000..361528d
--- /dev/null
+++ b/static/js/app/components/builds/list.js
@@ -0,0 +1,39 @@
+'use strict';
+
+define([
+ 'react',
+ 'reflux',
+ 'underscore',
+ './item',
+ 'app/actions/build',
+ 'app/stores/builds',
+ 'templates/app/components/builds/list'
+], function(React, Reflux, _, Item, BuildActions, buildsStore, template) {
+ template = template.locals({
+ Item: Item
+ });
+
+ var Component = React.createClass({
+ mixins: [
+ Reflux.connectFilter(buildsStore, 'items', function(items) {
+ var projectName = this.props.projectName;
+ if (projectName) {
+ return _(items).filter(function(item) {
+ return item.project && item.project.name === projectName;
+ });
+ } else {
+ return items;
+ }
+ })
+ ],
+ onShowMoreBuilds: function(projectName) {
+ BuildActions.readAll({
+ projectName: projectName,
+ limit: this.state.items.length + 20
+ });
+ },
+ render: template
+ });
+
+ return Component;
+});
diff --git a/static/js/app/components/builds/view.js b/static/js/app/components/builds/view.js
new file mode 100644
index 0000000..0531024
--- /dev/null
+++ b/static/js/app/components/builds/view.js
@@ -0,0 +1,66 @@
+'use strict';
+
+define([
+ 'react',
+ 'react-router',
+ 'reflux',
+ 'app/actions/build',
+ 'app/stores/build',
+ 'app/components/terminal/terminal',
+ 'app/components/buildSidebar/index',
+ 'templates/app/components/builds/view',
+ 'app/components/common/index'
+], function(
+ React, Router, Reflux, BuildActions, buildStore, TerminalComponent,
+ BuildSidebar, template, CommonComponents
+) {
+ template = template.locals({
+ DateTime: CommonComponents.DateTime,
+ Duration: CommonComponents.Duration,
+ Scm: CommonComponents.Scm,
+ Terminal: TerminalComponent,
+ Link: Router.Link,
+ BuildSidebar: BuildSidebar
+ });
+
+ var Component = React.createClass({
+ mixins: [Reflux.ListenerMixin],
+ statics: {
+ willTransitionTo: function(transition, params, query) {
+ BuildActions.read(Number(params.id));
+ }
+ },
+ componentDidMount: function() {
+ this.listenTo(buildStore, this.updateBuild);
+ },
+ componentWillReceiveProps: function(nextProps) {
+ // reset console status when go from build page to another build
+ // page (did mount and mount not called in this case)
+ if (Number(nextProps.params.id) !== this.state.build.id) {
+ this.setState({showConsole: this.getInitialState().showConsole});
+ }
+ },
+ updateBuild: function(build) {
+ if (build) {
+ BuildActions.readAll({projectName: build.project.name});
+ }
+ this.setState({build: build});
+ },
+ render: template,
+ getInitialState: function() {
+ return {
+ build: null,
+ showConsole: false
+ };
+ },
+ toggleConsole: function() {
+ var consoleState = !this.state.showConsole;
+ if (consoleState) {
+ BuildActions.readTerminalOutput(this.state.build);
+ }
+ this.setState({showConsole: consoleState});
+ }
+ });
+
+ return Component;
+});
diff --git a/static/js/app/components/terminal/terminal.js b/static/js/app/components/terminal/terminal.js
new file mode 100644
index 0000000..83ca011
--- /dev/null
+++ b/static/js/app/components/terminal/terminal.js
@@ -0,0 +1,131 @@
+'use strict';
+
+define([
+ 'underscore',
+ 'react',
+ 'reflux',
+ 'app/stores/terminal',
+ 'app/stores/build',
+ 'ansi_up',
+ 'templates/app/components/terminal/terminal'
+], function(
+ _,
+ React,
+ Reflux,
+ terminalStore,
+ buildStore,
+ ansiUp,
+ template
+) {
+ var Component = React.createClass({
+ mixins: [Reflux.ListenerMixin],
+
+ shouldScrollBottom: true,
+ data: [],
+ linesCount: 0,
+
+ componentDidMount: function() {
+ this.listenTo(terminalStore, this.updateItems);
+ var node = document.getElementsByClassName('terminal')[0];
+ this.initialScrollPosition = node.getBoundingClientRect().top;
+ if (this.props.showPreloader) {
+ this.getTerminal().insertAdjacentHTML('afterend',
+ ''
+ );
+
+ this.listenTo(buildStore, function(build) {
+ if (build.completed) {
+ this.removePreloader();
+ }
+ });
+ }
+
+ window.onscroll = this.onScroll;
+ },
+ removePreloader: function() {
+ var preloader = document.getElementsByClassName(
+ 'terminal_preloader'
+ )[0];
+ preloader.parentNode.removeChild(preloader);
+ },
+ componentWillUnmount: function() {
+ window.onscroll = null;
+ },
+ prepareRow: function(row) {
+ return ansiUp.ansi_to_html(row.replace('\r', ''));
+ },
+ prepareOutput: function(output) {
+ var self = this;
+ return output.map(function(row) {
+ return self.prepareRow(row);
+ });
+ },
+ getTerminal: function() {
+ return document.getElementsByClassName('terminal')[0];
+ },
+ getBody: function() {
+ return document.getElementsByTagName('body')[0];
+ },
+ onScroll: function() {
+ var node = this.getTerminal(),
+ body = this.getBody();
+
+ this.shouldScrollBottom = window.innerHeight + body.scrollTop >=
+ node.offsetHeight + this.initialScrollPosition;
+ },
+ ensureScrollPosition: function() {
+ if (this.shouldScrollBottom) {
+ var node = this.getTerminal(),
+ body = this.getBody();
+
+ body.scrollTop = this.initialScrollPosition + node.offsetHeight;
+ }
+ },
+ makeCodeLineContent: function(line) {
+ return '' + '' +
+ '