diff --git a/docs/en/install/README.md b/docs/en/install/README.md
index 21df941bfa..268c11af5b 100644
--- a/docs/en/install/README.md
+++ b/docs/en/install/README.md
@@ -508,6 +508,11 @@ See docs of the specified route and `lib/config.js` for detailed information.
:::
+- Bitbucket: [Basic auth with App passwords](https://developer.atlassian.com/cloud/bitbucket/rest/intro/#basic-auth)
+
+ - `BITBUCKET_USERNAME`: Your Bitbucket username
+ - `BITBUCKET_PASSWORD`: Your Bitbucket app password
+
- Discuz cookie
- `DISCUZ_COOKIE_{cid}`: Cookie of a forum powered by Discuz, cid can be anything from 00 to 99. When visiting a Discuz route, use cid to specify this cookie.
diff --git a/docs/en/programming.md b/docs/en/programming.md
index 4a926715c1..48bd46e4e2 100644
--- a/docs/en/programming.md
+++ b/docs/en/programming.md
@@ -205,6 +205,16 @@ For instance, the `/github/topics/framework/l=php&o=desc&s=stars` route will gen
+## Bitbucket
+
+### Commits
+
+
+
+### Tags
+
+
+
## Gitpod
### Blog
diff --git a/docs/install/README.md b/docs/install/README.md
index 4f8f90cc88..547087c3da 100644
--- a/docs/install/README.md
+++ b/docs/install/README.md
@@ -525,6 +525,11 @@ RSSHub 支持使用访问密钥 / 码,白名单和黑名单三种方式进行
3. 点击 dynamic_new 请求,找到 Cookie
4. 视频和专栏只要求 `SESSDATA` 字段,动态需复制整段 Cookie
+- Bitbucket: [Basic auth with App passwords](https://developer.atlassian.com/cloud/bitbucket/rest/intro/#basic-auth)
+
+ - `BITBUCKET_USERNAME`: 你的 Bitbucket 用户名
+ - `BITBUCKET_PASSWORD`: 你的 Bitbucket 密码
+
- BTBYR
- `BTBYR_HOST`: 支持 ipv4 访问的 BTBYR 镜像,默认为原站 `https://bt.byr.cn/`。
diff --git a/docs/programming.md b/docs/programming.md
index 123294f0fa..44c4aa74a7 100644
--- a/docs/programming.md
+++ b/docs/programming.md
@@ -283,6 +283,16 @@ GitHub 官方也提供了一些 RSS:
+## Bitbucket
+
+### Commits
+
+
+
+### Tags
+
+
+
## Gitpod
### 博客
diff --git a/lib/config.js b/lib/config.js
index 337c5d3161..8e86710e8a 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -91,6 +91,10 @@ const calculateValue = () => {
bilibili: {
cookies: bilibili_cookies,
},
+ bitbucket: {
+ username: envs.BITBUCKET_USERNAME,
+ password: envs.BITBUCKET_PASSWORD,
+ },
btbyr: {
host: envs.BTBYR_HOST,
cookies: envs.BTBYR_COOKIE,
diff --git a/lib/v2/bitbucket/commits.js b/lib/v2/bitbucket/commits.js
new file mode 100644
index 0000000000..f590281ea8
--- /dev/null
+++ b/lib/v2/bitbucket/commits.js
@@ -0,0 +1,40 @@
+const got = require('@/utils/got');
+const config = require('@/config').value;
+const queryString = require('query-string');
+const { parseDate } = require('@/utils/parse-date');
+
+module.exports = async (ctx) => {
+ const workspace = ctx.params.workspace;
+ const repo_slug = ctx.params.repo_slug;
+
+ const headers = {
+ Accept: 'application/json',
+ };
+ let auth = '';
+ if (config.bitbucket && config.bitbucket.username && config.bitbucket.password) {
+ auth = config.bitbucket.username + ':' + config.bitbucket.password + '@';
+ }
+ const response = await got({
+ method: 'get',
+ url: `https://${auth}api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/commits/`,
+ searchParams: queryString.stringify({
+ sort: '-target.date',
+ }),
+ headers,
+ });
+ const data = response.data.values;
+ ctx.state.data = {
+ allowEmpty: true,
+ title: `Recent Commits to ${workspace}/${repo_slug}`,
+ link: `https://bitbucket.org/${workspace}/${repo_slug}`,
+ item:
+ data &&
+ data.map((item) => ({
+ title: item.message,
+ author: item.author.raw,
+ description: item.rendered.message.html || 'No description',
+ pubDate: parseDate(item.date),
+ link: item.links.html.href,
+ })),
+ };
+};
diff --git a/lib/v2/bitbucket/maintainer.js b/lib/v2/bitbucket/maintainer.js
new file mode 100644
index 0000000000..fae9e648b6
--- /dev/null
+++ b/lib/v2/bitbucket/maintainer.js
@@ -0,0 +1,4 @@
+module.exports = {
+ '/commits/:workspace/:repo_slug': ['AuroraDysis'],
+ '/tags/:workspace/:repo_slug': ['AuroraDysis'],
+};
diff --git a/lib/v2/bitbucket/radar.js b/lib/v2/bitbucket/radar.js
new file mode 100644
index 0000000000..89ea14c904
--- /dev/null
+++ b/lib/v2/bitbucket/radar.js
@@ -0,0 +1,19 @@
+module.exports = {
+ 'bitbucket.com': {
+ _name: 'Bitbucket',
+ '.': [
+ {
+ title: 'Commits',
+ docs: 'https://docs.rsshub.app/programming.html#bitbucket',
+ source: ['/commits/:workspace/:repo_slug'],
+ target: '/bitbucket/commits/:workspace/:repo_slug',
+ },
+ {
+ title: 'Tags',
+ docs: 'https://docs.rsshub.app/programming.html#bitbucket',
+ source: ['/tags/:workspace/:repo_slug'],
+ target: '/bitbcuket/tags/:workspace/:repo_slug',
+ },
+ ],
+ },
+};
diff --git a/lib/v2/bitbucket/router.js b/lib/v2/bitbucket/router.js
new file mode 100644
index 0000000000..40bbe46f35
--- /dev/null
+++ b/lib/v2/bitbucket/router.js
@@ -0,0 +1,4 @@
+module.exports = (router) => {
+ router.get('/commits/:workspace/:repo_slug', require('./commits'));
+ router.get('/tags/:workspace/:repo_slug', require('./tags'));
+};
diff --git a/lib/v2/bitbucket/tags.js b/lib/v2/bitbucket/tags.js
new file mode 100644
index 0000000000..abb346f87f
--- /dev/null
+++ b/lib/v2/bitbucket/tags.js
@@ -0,0 +1,40 @@
+const got = require('@/utils/got');
+const config = require('@/config').value;
+const queryString = require('query-string');
+const { parseDate } = require('@/utils/parse-date');
+
+module.exports = async (ctx) => {
+ const workspace = ctx.params.workspace;
+ const repo_slug = ctx.params.repo_slug;
+
+ const headers = {
+ Accept: 'application/json',
+ };
+ let auth = '';
+ if (config.bitbucket && config.bitbucket.username && config.bitbucket.password) {
+ auth = config.bitbucket.username + ':' + config.bitbucket.password + '@';
+ }
+ const response = await got({
+ method: 'get',
+ url: `https://${auth}api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/refs/tags/`,
+ searchParams: queryString.stringify({
+ sort: '-target.date',
+ }),
+ headers,
+ });
+ const data = response.data.values;
+ ctx.state.data = {
+ allowEmpty: true,
+ title: `Recent Tags in ${workspace}/${repo_slug}`,
+ link: `https://bitbucket.org/${workspace}/${repo_slug}`,
+ item:
+ data &&
+ data.map((item) => ({
+ title: item.name,
+ author: item.tagger.raw,
+ description: item.message || 'No description',
+ pubDate: parseDate(item.date),
+ link: item.links.html.href,
+ })),
+ };
+};