Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Build

on:
workflow_dispatch: {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Note to self: Why is this needed?

push:
branches:
- '**'
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ Stay in the flow by using Atlassian for VSCode to start work on a JIRA issue, ra

[**Download now**](https://marketplace.visualstudio.com/items?itemName=Atlassian.atlascode&ssr=false#overview)

## [Devsphere specific changelog](/DEVSPHERE_CHANGELOG.md)
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove these logs: these are not useful in upstream

We're adding certain features & updates to this fork which may or may not be suitable to push upstream. Refer to the [Devsphere specific changelog](/DEVSPHERE_CHANGELOG.md) to get more context.

## Usage


### Getting Started

- Make sure you have VS Code version 1.77.0 or above
Expand Down
44 changes: 42 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@
},
"category": "Atlassian"
},
{
"command": "atlascode.devsphere.review.initialise",
"title": "Initialise Devsphere Review Settings",
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to "Unset Pull Request Review mode"

"category": "Atlassian"
},
{
"command": "atlascode.devsphere.custom.reset",
"title": "Reset Devsphere Custom Configuration",
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to "Set Pull Request Review mode"

"category": "Atlassian"
},
{
"command": "atlascode.jira.searchIssues",
"title": "Search Jira Issue Results",
Expand Down Expand Up @@ -402,6 +412,14 @@
"title": "Disable Help Explorer",
"category": "Atlassian"
},
{
"command": "atlascode.bitbucket.pullRequestsOverview.refresh",
"title": "Refresh",
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to "Refresh Pull Request Overview Panel"

"icon": {
"light": "resources/light/refresh.svg",
"dark": "resources/dark/refresh.svg"
}
},
{
"command": "atlascode.showOnboardingFlow",
"title": "Show Onboarding Flow",
Expand Down Expand Up @@ -432,7 +450,12 @@
{
"id": "atlascode.views.bb.pullrequestsTreeView",
"name": "Bitbucket pull requests",
"when": "atlascode:bitbucketExplorerEnabled && config.atlascode.bitbucket.enabled"
"when": "atlascode:bitbucketExplorerEnabled && config.atlascode.bitbucket.enabled && config.atlascode.bitbucket.explorer.repositoryBasedPullRequestView.enabled"
},
{
"id": "atlascode.views.bb.pullRequestsOverviewTreeView",
"name": "Bitbucket pull requests overview",
"when": "atlascode:bitbucketExplorerEnabled && config.atlascode.bitbucket.enabled && config.atlascode.bitbucket.explorer.pullRequestsOverview.enabled"
},
{
"id": "atlascode.views.bb.pipelinesTreeView",
Expand Down Expand Up @@ -579,6 +602,11 @@
{
"command": "atlascode.disableHelpExplorer",
"when": "view == atlascode.views.helpTreeView"
},
{
"command": "atlascode.bitbucket.pullRequestsOverview.refresh",
"when": "view == atlascode.views.bb.pullRequestsOverviewTreeView",
"group": "navigation@1"
}
],
"view/item/context": [
Expand Down Expand Up @@ -1105,6 +1133,18 @@
"description": "Enables the Bitbucket Pull Request Explorer",
"scope": "window"
},
"atlascode.bitbucket.explorer.repositoryBasedPullRequestView.enabled": {
"type": "boolean",
"default": true,
"description": "Enable repository based pull requests tree view",
"scope": "window"
},
"atlascode.bitbucket.explorer.pullRequestsOverview.enabled": {
"type": "boolean",
"default": true,
"description": "Enable pull requests overview tree view",
"scope": "window"
},
"atlascode.bitbucket.explorer.nestFilesEnabled": {
"type": "boolean",
"default": true,
Expand Down Expand Up @@ -1408,4 +1448,4 @@
"webpack-node-externals": "^3.0.0"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}
6 changes: 6 additions & 0 deletions src/atlclients/clientManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ConfigurationChangeEvent, Disposable, ExtensionContext } from 'vscode';
import { commands, window } from 'vscode';

import { CloudPullRequestApi } from '../bitbucket/bitbucket-cloud/pullRequests';
import { PullRequestsOverviewApi } from '../bitbucket/bitbucket-cloud/pullRequestsOverview';
import { CloudRepositoriesApi } from '../bitbucket/bitbucket-cloud/repositories';
import { ServerPullRequestApi } from '../bitbucket/bitbucket-server/pullRequests';
import { ServerRepositoriesApi } from '../bitbucket/bitbucket-server/repositories';
Expand Down Expand Up @@ -124,6 +125,9 @@ export class ClientManager implements Disposable {
pullrequests: isOAuthInfo(info)
? new CloudPullRequestApi(this.createOAuthHTTPClient(site, info.access))
: undefined!,
pullrequestsOverview: isOAuthInfo(info)
? new PullRequestsOverviewApi(this.createOAuthHTTPClient(site, info.access))
: undefined!,
pipelines: isOAuthInfo(info)
? new PipelineApiImpl(this.createOAuthHTTPClient(site, info.access))
: undefined!,
Expand All @@ -138,6 +142,8 @@ export class ClientManager implements Disposable {
isBasicAuthInfo(info) || isPATAuthInfo(info)
? new ServerPullRequestApi(this.createHTTPClient(site, info))
: undefined!,
// Note: For now Internal Pull Requests are not supported for Server
pullrequestsOverview: undefined,
pipelines: undefined,
};
}
Expand Down
150 changes: 150 additions & 0 deletions src/atlclients/strategyCrypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Mock crypto module at the top
class MockHash {
private data: string = '';

update(data: any): this {
this.data += data.toString();
return this;
}

digest(): Buffer {
// Return a predictable hash for testing
const hash = Buffer.alloc(32);
for (let i = 0; i < 32; i++) {
hash[i] = (this.data.charCodeAt(i % this.data.length) + i) % 256;
}
return hash;
}
}

jest.mock('crypto', () => ({
default: {
createHash: () => new MockHash(),
randomBytes: (size: number) => {
// Return predictable bytes for testing
const buffer = Buffer.alloc(size);
for (let i = 0; i < size; i++) {
buffer[i] = i % 256;
}
return buffer;
},
},
}));

import { base64URLEncode, basicAuth, createVerifier, sha256 } from './strategyCrypto';

describe('strategyCrypto', () => {
describe('basicAuth', () => {
it('should create basic auth string with username and password', () => {
const result = basicAuth('testuser', 'testpass');
expect(result).toBe('Basic dGVzdHVzZXI6dGVzdHBhc3M=');
});

it('should handle empty username and password', () => {
const result = basicAuth('', '');
expect(result).toBe('Basic Og==');
});

it('should handle special characters in username and password', () => {
const result = basicAuth('[email protected]', 'p@ssw0rd!');
expect(result).toBe('Basic dXNlckBkb21haW4uY29tOnBAc3N3MHJkIQ==');
});

it('should handle unicode characters', () => {
const result = basicAuth('用户', '密码');
expect(result).toContain('Basic ');
expect(result.length).toBeGreaterThan('Basic '.length);
});
});

describe('base64URLEncode', () => {
it('should encode buffer to base64 URL safe string', () => {
const buffer = Buffer.from('hello world');
const result = base64URLEncode(buffer);
expect(result).toBe('aGVsbG8gd29ybGQ');
});

it('should replace URL unsafe characters', () => {
// Create a buffer that will have + and / characters in base64
const buffer = Buffer.from('hello>world?test');
const result = base64URLEncode(buffer);
expect(result).not.toContain('+');
expect(result).not.toContain('/');
expect(result).not.toContain('=');
});

it('should handle empty buffer', () => {
const buffer = Buffer.from('');
const result = base64URLEncode(buffer);
expect(result).toBe('');
});

it('should handle binary data', () => {
const buffer = Buffer.from([0x00, 0x01, 0x02, 0x03, 0xff]);
const result = base64URLEncode(buffer);
expect(result).toBe('AAECA_8'); // Corrected expected value
});
});

describe('sha256', () => {
it('should create sha256 hash from string', () => {
const result = sha256('hello world');
expect(result).toBeInstanceOf(Buffer);
expect(result.length).toBe(32); // SHA256 produces 32 bytes
});

it('should create sha256 hash from buffer', () => {
const buffer = Buffer.from('test data');
const result = sha256(buffer);
expect(result).toBeInstanceOf(Buffer);
expect(result.length).toBe(32);
});

it('should produce consistent hash for same input', () => {
const input = 'consistent input';
const result1 = sha256(input);
const result2 = sha256(input);
expect(result1.equals(result2)).toBe(true);
});

it('should produce different hashes for different inputs', () => {
const result1 = sha256('input1');
const result2 = sha256('input2');
expect(result1.equals(result2)).toBe(false);
});

it('should handle empty string', () => {
const result = sha256('');
expect(result).toBeInstanceOf(Buffer);
expect(result.length).toBe(32);
});
});

describe('createVerifier', () => {
it('should create a verifier string', () => {
const result = createVerifier();
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
});

it('should create consistent verifiers with mocked crypto', () => {
const result1 = createVerifier();
const result2 = createVerifier();
// With our mock, results should be the same
expect(result1).toBe(result2);
});

it('should create URL-safe verifier string', () => {
const result = createVerifier();
expect(result).not.toContain('+');
expect(result).not.toContain('/');
expect(result).not.toContain('=');
});

it('should create verifier of expected length', () => {
const result = createVerifier();
// 32 bytes base64URL encoded should be 43 characters (without padding)
expect(result.length).toBe(43);
});
});
});
4 changes: 4 additions & 0 deletions src/bitbucket/bbContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CacheMap } from '../util/cachemap';
import { Time } from '../util/time';
import { PullRequestCommentController } from '../views/pullrequest/prCommentController';
import { PullRequestsExplorer } from '../views/pullrequest/pullRequestsExplorer';
import { PullRequestsOverviewExplorer } from '../views/pullrequest/PullRequestsOverviewExplorer';
import { clientForSite, getBitbucketCloudRemotes, getBitbucketRemotes, workspaceRepoFor } from './bbUtils';
import { BitbucketSite, PullRequest, User, WorkspaceRepo } from './model';

Expand All @@ -21,6 +22,7 @@ export class BitbucketContext extends Disposable {
private _gitApi: GitApi;
private _repoMap: Map<string, WorkspaceRepo> = new Map();
private _pullRequestsExplorer: PullRequestsExplorer;
private _pullRequestsOverviewExplorer: PullRequestsOverviewExplorer;
private _disposable: Disposable;
private _currentUsers: CacheMap;
private _pullRequestCache = new CacheMap();
Expand All @@ -31,6 +33,7 @@ export class BitbucketContext extends Disposable {
super(() => this.dispose());
this._gitApi = gitApi;
this._pullRequestsExplorer = new PullRequestsExplorer(this);
this._pullRequestsOverviewExplorer = new PullRequestsOverviewExplorer(this);
this._currentUsers = new CacheMap();

Container.context.subscriptions.push(
Expand All @@ -48,6 +51,7 @@ export class BitbucketContext extends Disposable {
this._gitApi.onDidOpenRepository(() => this.refreshRepos()),
this._gitApi.onDidCloseRepository(() => this.refreshRepos()),
this._pullRequestsExplorer,
this._pullRequestsOverviewExplorer,
this.prCommentController,
);

Expand Down
46 changes: 46 additions & 0 deletions src/bitbucket/bbUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { encodePathParts } from './bbUtils';

describe('bbUtils', () => {
describe('encodePathParts', () => {
it('should encode path parts with special characters', () => {
const path = 'folder/sub folder/file with spaces.txt';
const result = encodePathParts(path);
expect(result).toBe('folder/sub%20folder/file%20with%20spaces.txt');
});

it('should handle paths with special URL characters', () => {
const path = 'folder/sub&folder/file?param=value.txt';
const result = encodePathParts(path);
expect(result).toBe('folder/sub%26folder/file%3Fparam%3Dvalue.txt');
});

it('should handle empty path parts', () => {
const path = 'folder//subfolder';
const result = encodePathParts(path);
expect(result).toBe('folder//subfolder');
});

it('should handle single folder name', () => {
const path = 'folder name';
const result = encodePathParts(path);
expect(result).toBe('folder%20name');
});

it('should handle empty string', () => {
const path = '';
const result = encodePathParts(path);
expect(result).toBe('');
});

it('should handle null/undefined input', () => {
expect(encodePathParts(null as any)).toBe(undefined);
expect(encodePathParts(undefined as any)).toBe(undefined);
});

it('should handle paths with unicode characters', () => {
const path = 'folder/测试文件/файл.txt';
const result = encodePathParts(path);
expect(result).toBe('folder/%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6/%D1%84%D0%B0%D0%B9%D0%BB.txt');
});
});
});
Loading