2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package git4idea
.checkin
;
18 import com
.intellij
.openapi
.progress
.ProgressManager
;
19 import com
.intellij
.openapi
.project
.Project
;
20 import com
.intellij
.openapi
.ui
.DialogWrapper
;
21 import com
.intellij
.openapi
.ui
.Messages
;
22 import com
.intellij
.openapi
.vcs
.VcsException
;
23 import com
.intellij
.openapi
.vfs
.VirtualFile
;
24 import com
.intellij
.ui
.CheckboxTree
;
25 import com
.intellij
.ui
.CheckedTreeNode
;
26 import com
.intellij
.ui
.ColoredTreeCellRenderer
;
27 import com
.intellij
.ui
.SimpleTextAttributes
;
28 import com
.intellij
.util
.ui
.tree
.TreeUtil
;
29 import git4idea
.GitBranch
;
30 import git4idea
.GitRevisionNumber
;
31 import git4idea
.GitVcs
;
32 import git4idea
.actions
.GitShowAllSubmittedFilesAction
;
33 import git4idea
.commands
.*;
34 import git4idea
.i18n
.GitBundle
;
35 import git4idea
.ui
.GitUIUtil
;
38 import javax
.swing
.event
.TreeSelectionEvent
;
39 import javax
.swing
.event
.TreeSelectionListener
;
40 import javax
.swing
.tree
.DefaultMutableTreeNode
;
41 import javax
.swing
.tree
.DefaultTreeModel
;
42 import javax
.swing
.tree
.TreePath
;
43 import java
.awt
.event
.ActionEvent
;
44 import java
.awt
.event
.ActionListener
;
48 * The dialog that allows pushing active branches.
50 public class GitPushActiveBranchesDialog
extends DialogWrapper
{
52 * Amount of digits to show in commit prefix
54 private final static int HASH_PREFIX_SIZE
= 8;
56 * The view commit button
58 private JButton myViewButton
;
62 private JPanel myPanel
;
64 * Fetch changes from remote repository
66 private JButton myFetchButton
;
68 * Rebase commits to new roots
70 private JButton myRebaseButton
;
72 * If selected, the changes are auto-stashed before rebase
74 private JCheckBox myAutoStashCheckBox
;
76 * The commit tree (sorted by vcs roots)
78 private CheckboxTree myCommitTree
;
82 private CheckedTreeNode myTreeRoot
;
86 private Project myProject
;
88 * The vcs roots for the project
90 private List
<VirtualFile
> myVcsRoots
;
95 * @param project the project
96 * @param vcsRoots the vcs roots
97 * @param roots the loaded information about roots
99 private GitPushActiveBranchesDialog(final Project project
, List
<VirtualFile
> vcsRoots
, List
<Root
> roots
) {
100 super(project
, true);
102 myVcsRoots
= vcsRoots
;
103 updateTree(roots
, null);
104 TreeUtil
.expandAll(myCommitTree
);
105 myCommitTree
.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
106 public void valueChanged(TreeSelectionEvent e
) {
107 TreePath path
= myCommitTree
.getSelectionModel().getSelectionPath();
109 myViewButton
.setEnabled(false);
112 DefaultMutableTreeNode node
= (DefaultMutableTreeNode
)path
.getLastPathComponent();
113 myViewButton
.setEnabled(node
!= null && myCommitTree
.getSelectionCount() == 1 && node
.getUserObject() instanceof Commit
);
116 myViewButton
.addActionListener(new ActionListener() {
117 public void actionPerformed(ActionEvent e
) {
118 TreePath path
= myCommitTree
.getSelectionModel().getSelectionPath();
122 DefaultMutableTreeNode node
= (DefaultMutableTreeNode
)path
.getLastPathComponent();
123 if (node
== null || !(node
.getUserObject() instanceof Commit
)) {
126 Commit c
= (Commit
)node
.getUserObject();
127 GitShowAllSubmittedFilesAction
.showSubmittedFiles(project
, c
.revision
.asString(), c
.root
.root
);
130 myFetchButton
.addActionListener(new ActionListener() {
131 public void actionPerformed(ActionEvent e
) {
135 myRebaseButton
.addActionListener(new ActionListener() {
136 public void actionPerformed(ActionEvent e
) {
140 setTitle(GitBundle
.getString("push.active.title"));
141 setOKButtonText(GitBundle
.getString("push.active.button"));
146 * Perform fetch operation
148 private void doFetch() {
149 Map
<VirtualFile
, Set
<String
>> unchecked
= new HashMap
<VirtualFile
, Set
<String
>>();
150 for (int i
= 0; i
< myTreeRoot
.getChildCount(); i
++) {
151 Set
<String
> uncheckedCommits
= new HashSet
<String
>();
152 CheckedTreeNode node
= (CheckedTreeNode
)myTreeRoot
.getChildAt(i
);
153 Root r
= (Root
)node
.getUserObject();
154 for (int j
= 0; j
< node
.getChildCount(); j
++) {
155 if (node
.getChildAt(j
) instanceof CheckedTreeNode
) {
156 CheckedTreeNode commitNode
= (CheckedTreeNode
)node
.getChildAt(j
);
157 if (!commitNode
.isChecked()) {
158 uncheckedCommits
.add(((Commit
)commitNode
.getUserObject()).commitId());
162 if (!uncheckedCommits
.isEmpty()) {
163 unchecked
.put(r
.root
, uncheckedCommits
);
166 refreshTree(true, unchecked
);
170 * The rebase operation is needed if the current branch is behind remote branch or if some commit is not selected.
172 * @return true if rebase is needed for at least one vcs root
174 private boolean isRebaseNeeded() {
175 for (int i
= 0; i
< myTreeRoot
.getChildCount(); i
++) {
176 CheckedTreeNode node
= (CheckedTreeNode
)myTreeRoot
.getChildAt(i
);
177 Root r
= (Root
)node
.getUserObject();
178 if (r
.commits
.size() == 0) {
181 boolean seenCheckedNode
= false;
182 for (int j
= 0; j
< node
.getChildCount(); j
++) {
183 if (node
.getChildAt(j
) instanceof CheckedTreeNode
) {
184 CheckedTreeNode commitNode
= (CheckedTreeNode
)node
.getChildAt(j
);
185 if (commitNode
.isChecked()) {
186 seenCheckedNode
= true;
189 if (seenCheckedNode
) {
195 if (seenCheckedNode
&& r
.remoteCommits
> 0) {
203 * Preform rebase operation
205 private void doRebase() {
206 final Set
<VirtualFile
> roots
= new HashSet
<VirtualFile
>();
207 final Set
<VirtualFile
> rootsWithMerges
= new HashSet
<VirtualFile
>();
208 final Map
<VirtualFile
, List
<String
>> reorderedCommits
= new HashMap
<VirtualFile
, List
<String
>>();
209 final Map
<VirtualFile
, Set
<String
>> uncheckedCommits
= new HashMap
<VirtualFile
, Set
<String
>>();
210 for (int i
= 0; i
< myTreeRoot
.getChildCount(); i
++) {
211 CheckedTreeNode node
= (CheckedTreeNode
)myTreeRoot
.getChildAt(i
);
212 Root r
= (Root
)node
.getUserObject();
213 Set
<String
> unchecked
= new HashSet
<String
>();
214 uncheckedCommits
.put(r
.root
, unchecked
);
215 if (r
.commits
.size() == 0) {
218 boolean seenCheckedNode
= false;
219 boolean reorderNeeded
= false;
220 boolean seenMerges
= false;
221 for (int j
= 0; j
< node
.getChildCount(); j
++) {
222 if (node
.getChildAt(j
) instanceof CheckedTreeNode
) {
223 CheckedTreeNode commitNode
= (CheckedTreeNode
)node
.getChildAt(j
);
224 Commit commit
= (Commit
)commitNode
.getUserObject();
225 seenMerges
|= commit
.isMerge
;
226 if (commitNode
.isChecked()) {
227 seenCheckedNode
= true;
230 unchecked
.add(commit
.commitId());
231 if (seenCheckedNode
) {
232 reorderNeeded
= true;
238 rootsWithMerges
.add(r
.root
);
240 if (r
.remoteCommits
> 0 && seenCheckedNode
|| reorderNeeded
) {
244 List
<String
> reordered
= new ArrayList
<String
>();
245 for (int j
= 0; j
< node
.getChildCount(); j
++) {
246 if (node
.getChildAt(j
) instanceof CheckedTreeNode
) {
247 CheckedTreeNode commitNode
= (CheckedTreeNode
)node
.getChildAt(j
);
248 if (!commitNode
.isChecked()) {
249 Commit commit
= (Commit
)commitNode
.getUserObject();
250 reordered
.add(commit
.revision
.asString());
254 for (int j
= 0; j
< node
.getChildCount(); j
++) {
255 if (node
.getChildAt(j
) instanceof CheckedTreeNode
) {
256 CheckedTreeNode commitNode
= (CheckedTreeNode
)node
.getChildAt(j
);
257 if (commitNode
.isChecked()) {
258 Commit commit
= (Commit
)commitNode
.getUserObject();
259 reordered
.add(commit
.revision
.asString());
263 Collections
.reverse(reordered
);
264 reorderedCommits
.put(r
.root
, reordered
);
267 final List
<VcsException
> exceptions
= new ArrayList
<VcsException
>();
268 final boolean autoStash
= myAutoStashCheckBox
.isSelected();
269 final ProgressManager progressManager
= ProgressManager
.getInstance();
270 final GitVcs vcs
= GitVcs
.getInstance(myProject
);
271 progressManager
.runProcessWithProgressSynchronously(new Runnable() {
273 GitPushRebaseProcess process
= new GitPushRebaseProcess(vcs
, myProject
, exceptions
, autoStash
, reorderedCommits
, rootsWithMerges
);
274 process
.doUpdate(progressManager
.getProgressIndicator(), roots
);
276 }, GitBundle
.getString("push.active.rebasing"), false, myProject
);
277 refreshTree(false, uncheckedCommits
);
278 if (!exceptions
.isEmpty()) {
279 GitUIUtil
.showOperationErrors(myProject
, exceptions
, "git rebase");
286 * @param fetchData if true, the current state is fetched from remote
287 * @param unchecked the map from vcs root to commit identifiers that should be unchecked
289 private void refreshTree(final boolean fetchData
, Map
<VirtualFile
, Set
<String
>> unchecked
) {
290 ArrayList
<VcsException
> exceptions
= new ArrayList
<VcsException
>();
291 List
<Root
> roots
= loadRoots(myProject
, myVcsRoots
, exceptions
, fetchData
);
292 if (!exceptions
.isEmpty()) {
293 //noinspection ThrowableResultOfMethodCallIgnored
294 GitUIUtil
.showOperationErrors(myProject
, exceptions
, "Refreshing root information");
297 updateTree(roots
, unchecked
);
301 * Update the tree according to the list of loaded roots
303 * @param roots the list of roots to add to the tree
304 * @param uncheckedCommits the map from vcs root to commit identifiers that should be uncheckedCommits
306 private void updateTree(List
<Root
> roots
, Map
<VirtualFile
, Set
<String
>> uncheckedCommits
) {
307 myTreeRoot
.removeAllChildren();
308 for (Root r
: roots
) {
309 CheckedTreeNode rootNode
= new CheckedTreeNode(r
);
310 Status status
= new Status();
312 rootNode
.add(new DefaultMutableTreeNode(status
, false));
313 Set
<String
> unchecked
=
314 uncheckedCommits
!= null && uncheckedCommits
.containsKey(r
.root
) ? uncheckedCommits
.get(r
.root
) : Collections
.<String
>emptySet();
315 for (Commit c
: r
.commits
) {
316 CheckedTreeNode child
= new CheckedTreeNode(c
);
318 child
.setChecked(r
.remote
!= null && !unchecked
.contains(c
.commitId()));
320 myTreeRoot
.add(rootNode
);
322 ((DefaultTreeModel
)myCommitTree
.getModel()).reload(myTreeRoot
);
323 TreeUtil
.expandAll(myCommitTree
);
328 * Update buttons on the form
330 private void updateButtons() {
332 boolean wasCheckedNode
= false;
333 boolean reorderMerges
= false;
334 for (int i
= 0; i
< myTreeRoot
.getChildCount(); i
++) {
335 CheckedTreeNode node
= (CheckedTreeNode
)myTreeRoot
.getChildAt(i
);
336 boolean seenCheckedNode
= false;
337 boolean reorderNeeded
= false;
338 boolean seenMerges
= false;
339 boolean seenUnchecked
= false;
340 for (int j
= 0; j
< node
.getChildCount(); j
++) {
341 if (node
.getChildAt(j
) instanceof CheckedTreeNode
) {
342 CheckedTreeNode commitNode
= (CheckedTreeNode
)node
.getChildAt(j
);
343 Commit commit
= (Commit
)commitNode
.getUserObject();
344 seenMerges
|= commit
.isMerge
;
345 if (commitNode
.isChecked()) {
346 seenCheckedNode
= true;
349 seenUnchecked
= true;
350 if (seenCheckedNode
) {
351 reorderNeeded
= true;
356 if (!seenCheckedNode
) {
359 Root r
= (Root
)node
.getUserObject();
360 if (seenMerges
&& seenUnchecked
) {
361 error
= GitBundle
.getString("push.active.error.merges.unchecked");
363 if (seenMerges
&& reorderNeeded
) {
364 reorderMerges
= true;
365 error
= GitBundle
.getString("push.active.error.reorder.merges");
369 error
= GitBundle
.getString("push.active.error.reorder.needed");
372 if (r
.branch
== null) {
374 error
= GitBundle
.getString("push.active.error.no.branch");
378 wasCheckedNode
|= r
.remoteBranch
!= null;
379 if (r
.remoteCommits
!= 0 && r
.commits
.size() != 0) {
381 error
= GitBundle
.getString("push.active.error.behind");
386 boolean rebaseNeeded
= isRebaseNeeded();
387 setOKActionEnabled(wasCheckedNode
&& error
== null && !rebaseNeeded
);
389 myRebaseButton
.setEnabled(rebaseNeeded
&& !reorderMerges
);
396 protected JComponent
createCenterPanel() {
404 protected String
getDimensionServiceKey() {
405 return getClass().getName();
411 * @param project the project
412 * @param roots the VCS root list
413 * @param exceptions the list of of exceptions to use
414 * @param fetchData if true, the data for remote is fetched.
415 * @return the loaded information about vcs roots
417 static List
<Root
> loadRoots(final Project project
,
418 final List
<VirtualFile
> roots
,
419 final Collection
<VcsException
> exceptions
,
420 final boolean fetchData
) {
421 final ProgressManager manager
= ProgressManager
.getInstance();
422 final ArrayList
<Root
> rc
= new ArrayList
<Root
>();
423 manager
.runProcessWithProgressSynchronously(new Runnable() {
425 for (VirtualFile root
: roots
) {
430 GitBranch b
= GitBranch
.current(project
, root
);
432 r
.branch
= b
.getFullName();
433 r
.remote
= b
.getTrackedRemoteName(project
, root
);
434 r
.remoteBranch
= b
.getTrackedBranchName(project
, root
);
435 if (r
.remote
!= null) {
436 if (fetchData
&& !r
.remote
.equals(".")) {
437 GitLineHandler fetch
= new GitLineHandler(project
, root
, GitHandler
.FETCH
);
438 fetch
.addParameters(r
.remote
, "-v");
439 Collection
<VcsException
> exs
= GitHandlerUtil
.doSynchronouslyWithExceptions(fetch
);
440 exceptions
.addAll(exs
);
442 GitBranch tracked
= b
.tracked(project
, root
);
443 assert tracked
!= null : "Tracked branch cannot be null here";
444 GitSimpleHandler unmerged
= new GitSimpleHandler(project
, root
, GitHandler
.LOG
);
445 unmerged
.addParameters("--pretty=format:%H", r
.branch
+ ".." + tracked
.getFullName());
446 unmerged
.setNoSSH(true);
447 unmerged
.setStdoutSuppressed(true);
448 StringScanner su
= new StringScanner(unmerged
.run());
449 while (su
.hasMoreData()) {
450 if (su
.line().trim().length() != 0) {
454 GitSimpleHandler toPush
= new GitSimpleHandler(project
, root
, GitHandler
.LOG
);
455 toPush
.addParameters("--pretty=format:%H%x20%ct%x20%at%x20%s%n%P", tracked
.getFullName() + ".." + r
.branch
);
456 toPush
.setNoSSH(true);
457 toPush
.setStdoutSuppressed(true);
458 StringScanner sp
= new StringScanner(toPush
.run());
459 while (sp
.hasMoreData()) {
464 Commit c
= new Commit();
466 String hash
= sp
.spaceToken();
467 String time
= sp
.spaceToken();
468 c
.revision
= new GitRevisionNumber(hash
, new Date(Long
.parseLong(time
) * 1000L));
469 c
.authorTime
= sp
.spaceToken();
470 c
.message
= sp
.line();
471 c
.isMerge
= sp
.line().indexOf(' ') != -1;
477 catch (VcsException e
) {
482 }, GitBundle
.getString("push.active.fetching"), false, project
);
489 * @param project the context project
490 * @param vcsRoots the vcs roots in the project
491 * @param exceptions the collected exceptions
493 public static void showDialog(final Project project
, List
<VirtualFile
> vcsRoots
, final Collection
<VcsException
> exceptions
) {
494 final List
<Root
> roots
= loadRoots(project
, vcsRoots
, exceptions
, true);
495 if (!exceptions
.isEmpty()) {
497 .showErrorDialog(project
, GitBundle
.getString("push.active.fetch.failed"), GitBundle
.getString("push.active.fetch.failed.title"));
500 GitPushActiveBranchesDialog d
= new GitPushActiveBranchesDialog(project
, vcsRoots
, roots
);
503 final ArrayList
<Root
> rootsToPush
= new ArrayList
<Root
>();
504 for (int i
= 0; i
< d
.myTreeRoot
.getChildCount(); i
++) {
505 CheckedTreeNode node
= (CheckedTreeNode
)d
.myTreeRoot
.getChildAt(i
);
506 Root r
= (Root
)node
.getUserObject();
507 if (r
.remote
== null || r
.commits
.size() == 0) {
510 boolean topCommit
= true;
511 for (int j
= 0; j
< node
.getChildCount(); j
++) {
512 if (node
.getChildAt(j
) instanceof CheckedTreeNode
) {
513 CheckedTreeNode commitNode
= (CheckedTreeNode
)node
.getChildAt(j
);
514 if (commitNode
.isChecked()) {
515 Commit commit
= (Commit
)commitNode
.getUserObject();
517 r
.commitToPush
= commit
.revision
.asString();
526 final ProgressManager manager
= ProgressManager
.getInstance();
527 manager
.runProcessWithProgressSynchronously(new Runnable() {
529 for (Root r
: rootsToPush
) {
530 GitLineHandler h
= new GitLineHandler(project
, r
.root
, GitHandler
.PUSH
);
531 String src
= r
.commitToPush
!= null ? r
.commitToPush
: r
.branch
;
532 h
.addParameters("-v", r
.remote
, src
+ ":" + r
.remoteBranch
);
533 GitHandlerUtil
.doSynchronouslyWithExceptions(h
);
536 }, GitBundle
.getString("push.active.pushing"), false, project
);
541 * Create UI components for the dialog
543 private void createUIComponents() {
544 myTreeRoot
= new CheckedTreeNode("ROOT");
545 myCommitTree
= new CheckboxTree(new CheckboxTree
.CheckboxTreeCellRenderer() {
547 public void customizeRenderer(JTree tree
, Object value
, boolean selected
, boolean expanded
, boolean leaf
, int row
, boolean hasFocus
) {
548 ColoredTreeCellRenderer r
= getTextRenderer();
549 if (!(value
instanceof DefaultMutableTreeNode
)) {
551 renderUnknown(r
, value
);
554 DefaultMutableTreeNode node
= (DefaultMutableTreeNode
)value
;
555 if (!(node
.getUserObject() instanceof Node
)) {
557 renderUnknown(r
, node
.getUserObject());
560 ((Node
)node
.getUserObject()).render(r
);
564 * Render unknown node
566 * @param r a renderer to use
567 * @param value the unknown value
569 private void renderUnknown(ColoredTreeCellRenderer r
, Object value
) {
570 r
.append("UNSUPPORTED NODE TYPE: " + (value
== null ?
"null" : value
.getClass().getName()), SimpleTextAttributes
.ERROR_ATTRIBUTES
);
574 protected void onNodeStateChanged(CheckedTreeNode node
) {
576 super.onNodeStateChanged(node
);
583 * The base class for nodes in the tree
585 static abstract class Node
{
587 * Render the node text
589 * @param renderer the renderer to use
591 protected abstract void render(ColoredTreeCellRenderer renderer
);
595 * The commit descriptor
597 static class Status
extends Node
{
607 protected void render(ColoredTreeCellRenderer renderer
) {
608 renderer
.append(GitBundle
.getString("push.active.status.status"));
609 if (root
.branch
== null) {
610 renderer
.append(GitBundle
.message("push.active.status.no.branch"), SimpleTextAttributes
.ERROR_ATTRIBUTES
);
612 else if (root
.remote
== null) {
613 renderer
.append(GitBundle
.message("push.active.status.no.tracked"), SimpleTextAttributes
.GRAYED_BOLD_ATTRIBUTES
);
615 else if (root
.remoteCommits
!= 0 && root
.commits
.size() == 0) {
616 renderer
.append(GitBundle
.message("push.active.status.no.commits.behind", root
.remoteCommits
),
617 SimpleTextAttributes
.GRAYED_BOLD_ATTRIBUTES
);
619 else if (root
.commits
.size() == 0) {
620 renderer
.append(GitBundle
.message("push.active.status.no.commits"), SimpleTextAttributes
.GRAYED_BOLD_ATTRIBUTES
);
622 else if (root
.remoteCommits
!= 0) {
623 renderer
.append(GitBundle
.message("push.active.status.behind", root
.remoteCommits
), SimpleTextAttributes
.ERROR_ATTRIBUTES
);
626 renderer
.append(GitBundle
.message("push.active.status.push", root
.commits
.size()));
632 * The commit descriptor
634 static class Commit
extends Node
{
642 GitRevisionNumber revision
;
652 * If true, the commit is a merge
660 protected void render(ColoredTreeCellRenderer renderer
) {
661 renderer
.append(revision
.asString().substring(0, HASH_PREFIX_SIZE
), SimpleTextAttributes
.GRAYED_ATTRIBUTES
);
662 renderer
.append(": ");
663 renderer
.append(message
);
665 renderer
.append(GitBundle
.getString("push.active.commit.node.merge"), SimpleTextAttributes
.GRAYED_ATTRIBUTES
);
670 * @return the identifier that is supposed to be stable with respect to rebase
673 return authorTime
+ ":" + message
;
680 static class Root
extends Node
{
682 * if true, the update is required
686 * the path to vcs root
698 * the remote branch name
702 * The commit that will be actually pushed
708 List
<Commit
> commits
= new ArrayList
<Commit
>();
714 protected void render(ColoredTreeCellRenderer renderer
) {
715 SimpleTextAttributes rootAttributes
;
716 SimpleTextAttributes branchAttributes
;
717 if (remote
!= null && commits
.size() != 0 && remoteCommits
!= 0 || branch
== null) {
718 rootAttributes
= SimpleTextAttributes
.ERROR_ATTRIBUTES
.derive(SimpleTextAttributes
.STYLE_BOLD
, null, null, null);
719 branchAttributes
= SimpleTextAttributes
.ERROR_ATTRIBUTES
;
721 else if (remote
== null || commits
.size() == 0) {
722 rootAttributes
= SimpleTextAttributes
.GRAYED_BOLD_ATTRIBUTES
;
723 branchAttributes
= SimpleTextAttributes
.GRAYED_ATTRIBUTES
;
726 branchAttributes
= SimpleTextAttributes
.REGULAR_ATTRIBUTES
;
727 rootAttributes
= SimpleTextAttributes
.REGULAR_BOLD_ATTRIBUTES
;
729 renderer
.append(root
.getPresentableUrl(), rootAttributes
);
730 if (branch
!= null) {
731 renderer
.append(" [" + branch
, branchAttributes
);
732 if (remote
!= null) {
733 renderer
.append(" -> " + remote
+ "#" + remoteBranch
, branchAttributes
);
735 renderer
.append("]", branchAttributes
);